Created
June 6, 2025 05:48
-
-
Save qoli/ffd9b461f7224e5c144add943e450f3c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Directory Structure: | |
└── ./ | |
├── EDMesg | |
│ ├── __init__.py | |
│ ├── EDMesgBase.py | |
│ ├── EDMesgClient.py | |
│ └── EDMesgProvider.py | |
├── CargoParser.py | |
├── directinput.py | |
├── ED_AP.py | |
├── EDafk_combat.py | |
├── EDAP_data.py | |
├── EDAP_EDMesg_Client.py | |
├── EDAP_EDMesg_Interface.py | |
├── EDAP_EDMesg_Server.py | |
├── EDAPGui.py | |
├── EDGalaxyMap.py | |
├── EDGraphicsSettings.py | |
├── EDInternalStatusPanel.py | |
├── EDJournal.py | |
├── EDKeys.py | |
├── EDlogger.py | |
├── EDShipControl.py | |
├── EDStationServicesInShip.py | |
├── EDSystemMap.py | |
├── EDWayPoint.py | |
├── Image_Templates.py | |
├── MarketParser.py | |
├── MousePt.py | |
├── NavRouteParser.py | |
├── OCR.py | |
├── Overlay.py | |
├── Robigo.py | |
├── Screen_Regions.py | |
├── Screen.py | |
├── StatusParser.py | |
├── Test_Routines.py | |
├── Voice.py | |
└── WindowsKnownPaths.py | |
--- | |
File: /EDMesg/__init__.py | |
--- | |
--- | |
File: /EDMesg/EDMesgBase.py | |
--- | |
from abc import ABC | |
from pydantic import BaseModel | |
from typing import Any | |
class EDMesgAction(BaseModel, ABC): | |
pass | |
class EDMesgWelcomeAction(EDMesgAction): | |
pass | |
class EDMesgEvent(BaseModel, ABC): | |
pass | |
class EDMesgEnvelope(BaseModel): | |
type: str | |
data: dict[str, Any] | |
--- | |
File: /EDMesg/EDMesgClient.py | |
--- | |
import zmq | |
import threading | |
from queue import Queue | |
from time import sleep | |
import os | |
import tempfile | |
from typing import Any, final, Optional | |
from EDMesg.EDMesgBase import EDMesgEvent, EDMesgAction, EDMesgEnvelope, EDMesgWelcomeAction | |
@final | |
class EDMesgClient: | |
def __init__( | |
self, | |
provider_name: str, | |
action_types: list[type[EDMesgAction]], | |
event_types: list[type[EDMesgEvent]], | |
action_port: int, | |
event_port: int, | |
): | |
self.provider_name = provider_name | |
self.context = zmq.Context() | |
# Generate socket file paths based on provider's name | |
self.pub_socket_path = os.path.join( | |
tempfile.gettempdir(), f"edmesg_{self.provider_name}_pub.ipc" | |
) | |
self.pull_socket_path = os.path.join( | |
tempfile.gettempdir(), f"edmesg_{self.provider_name}_pull.ipc" | |
) | |
# Push socket for actions | |
self.push_socket = self.context.socket(zmq.PUSH) | |
# self.push_socket.connect(f"ipc://{self.pull_socket_path}") | |
self.push_socket.connect(f"tcp://127.0.0.1:{action_port}") | |
# Subscriber socket for events | |
self.sub_socket = self.context.socket(zmq.SUB) | |
# self.sub_socket.connect(f"ipc://{self.pub_socket_path}") | |
self.sub_socket.connect(f"tcp://127.0.0.1:{event_port}") | |
self.sub_socket.setsockopt_string(zmq.SUBSCRIBE, "") # Subscribe to all topics | |
# Queues for pending events | |
self.pending_events: Queue[EDMesgEvent] = Queue() | |
# Store action and event types | |
self.action_types = [EDMesgWelcomeAction] + action_types | |
self.event_types = event_types | |
# Start thread to listen for events | |
self._running = True | |
self.listener_thread = threading.Thread(target=self._listen_events, daemon=True) | |
self.listener_thread.start() | |
def publish(self, action: EDMesgAction): | |
envelope = EDMesgEnvelope( | |
type=action.__class__.__name__, data=action.model_dump() | |
) | |
message = envelope.model_dump_json() | |
self.push_socket.send_string(message) | |
def _listen_events(self): | |
while self._running: | |
try: | |
message = self.sub_socket.recv_string(flags=zmq.NOBLOCK) | |
envelope = EDMesgEnvelope.model_validate_json(message) | |
event = self._instantiate_event(envelope.type, envelope.data) | |
if event: | |
self.pending_events.put(event) | |
else: | |
print(f"Unknown event type received: {envelope.type}") | |
except zmq.Again: | |
sleep(0.01) # Prevent busy waiting | |
except Exception as e: | |
print(f"Error in _listen_events: {e}") | |
sleep(0.1) | |
def _instantiate_event( | |
self, type_name: str, data: dict[str, Any] | |
) -> Optional[EDMesgEvent]: | |
for event_class in self.event_types: | |
if event_class.__name__ == type_name: | |
return event_class(**data) | |
return None | |
def close(self): | |
self._running = False | |
self.listener_thread.join() | |
self.push_socket.close() | |
self.sub_socket.close() | |
self.context.term() | |
--- | |
File: /EDMesg/EDMesgProvider.py | |
--- | |
import zmq | |
import threading | |
from queue import Queue | |
from time import sleep | |
import os | |
import tempfile | |
from typing import Any, final, Optional | |
from EDMesg.EDMesgBase import EDMesgEvent, EDMesgAction, EDMesgEnvelope, EDMesgWelcomeAction | |
@final | |
class EDMesgProvider: | |
def __init__( | |
self, | |
provider_name: str, | |
action_types: list[type[EDMesgAction]], | |
event_types: list[type[EDMesgEvent]], | |
action_port: int, | |
event_port: int, | |
): | |
self.provider_name = provider_name | |
self.context = zmq.Context() | |
# Generate socket file paths | |
self.pub_socket_path = os.path.join( | |
tempfile.gettempdir(), f"edmesg_{self.provider_name}_pub.ipc" | |
) | |
self.pull_socket_path = os.path.join( | |
tempfile.gettempdir(), f"edmesg_{self.provider_name}_pull.ipc" | |
) | |
# Ensure any existing socket files are removed | |
for path in [self.pub_socket_path, self.pull_socket_path]: | |
if os.path.exists(path): | |
os.remove(path) | |
# Publisher socket for events | |
self.pub_socket = self.context.socket(zmq.PUB) | |
# self.pub_socket.bind(f"ipc://{self.pub_socket_path}") | |
self.pub_socket.bind(f"tcp://127.0.0.1:{event_port}") | |
self.pub_monitor = self.pub_socket.get_monitor_socket( | |
zmq.Event.HANDSHAKE_SUCCEEDED | |
) | |
# Pull socket for actions | |
self.pull_socket = self.context.socket(zmq.PULL) | |
# self.pull_socket.bind(f"ipc://{self.pull_socket_path}") | |
self.pull_socket.bind(f"tcp://127.0.0.1:{action_port}") | |
# Queues for pending actions | |
self.pending_actions: Queue[EDMesgAction] = Queue() | |
# Store action and event types | |
self.action_types = [EDMesgWelcomeAction] + action_types | |
self.event_types = event_types | |
# Start thread to listen for actions | |
self._running = True | |
self.listener_thread = threading.Thread( | |
target=self._listen_actions, daemon=True | |
) | |
self.listener_thread.start() | |
# Start thread to listen for status messages | |
self.status_thread = threading.Thread( | |
target=self._listen_status, daemon=True | |
) | |
self.status_thread.start() | |
def publish(self, event: EDMesgEvent): | |
envelope = EDMesgEnvelope( | |
type=event.__class__.__name__, data=event.model_dump() | |
) | |
message = envelope.model_dump_json() | |
self.pub_socket.send_string(message) | |
def _listen_actions(self): | |
while self._running: | |
try: | |
message = self.pull_socket.recv_string(flags=zmq.NOBLOCK) | |
envelope = EDMesgEnvelope.model_validate_json(message) | |
action = self._instantiate_action(envelope.type, envelope.data) | |
if action: | |
self.pending_actions.put(action) | |
else: | |
print(f"Unknown action type received: {envelope.type}") | |
except zmq.Again: | |
sleep(0.01) # Prevent busy waiting | |
except Exception as e: | |
print(f"Error in _listen_actions: {e}") | |
sleep(0.1) | |
def _listen_status(self): | |
while self._running: | |
try: | |
client = self.pub_monitor.recv_string(flags=zmq.NOBLOCK) | |
if client: | |
self.pending_actions.put(EDMesgWelcomeAction()) | |
except zmq.Again: | |
sleep(0.01) # Prevent busy waiting | |
except Exception as e: | |
print(f"Error in _listen_status: {e}") | |
sleep(0.1) | |
def _instantiate_action( | |
self, type_name: str, data: dict[str, Any] | |
) -> Optional[EDMesgAction]: | |
for action_class in self.action_types: | |
if action_class.__name__ == type_name: | |
return action_class(**data) | |
return None | |
def close(self): | |
self._running = False | |
self.pub_socket.close() | |
self.pull_socket.close() | |
self.pub_monitor.close() | |
self.context.term() | |
self.listener_thread.join() | |
self.status_thread.join() | |
# Clean up socket files | |
for path in [self.pub_socket_path, self.pull_socket_path]: | |
try: | |
os.remove(path) | |
except OSError: | |
pass | |
--- | |
File: /CargoParser.py | |
--- | |
from __future__ import annotations | |
import json | |
import os | |
import time | |
from datetime import datetime, timedelta | |
import queue | |
from sys import platform | |
import threading | |
from time import sleep | |
from EDlogger import logger | |
from WindowsKnownPaths import * | |
class CargoParser: | |
""" Parses the Cargo.json file generated by the game. """ | |
def __init__(self, file_path=None): | |
if platform != "win32": | |
self.file_path = file_path if file_path else "./linux_ed/Cargo.json" | |
else: | |
from WindowsKnownPaths import get_path, FOLDERID, UserHandle | |
self.file_path = file_path if file_path else (get_path(FOLDERID.SavedGames, UserHandle.current) | |
+ "/Frontier Developments/Elite Dangerous/Cargo.json") | |
self.last_mod_time = None | |
# Read json file data | |
self.current_data = self.get_cargo_data() | |
# self.watch_thread = threading.Thread(target=self._watch_file_thread, daemon=True) | |
# self.watch_thread.start() | |
# self.status_queue = queue.Queue() | |
# def _watch_file_thread(self): | |
# backoff = 1 | |
# while True: | |
# try: | |
# self._watch_file() | |
# except Exception as e: | |
# logger.debug('An error occurred when reading status file') | |
# sleep(backoff) | |
# logger.debug('Attempting to restart status file reader after failure') | |
# backoff *= 2 | |
# | |
# def _watch_file(self): | |
# """Detects changes in the Status.json file.""" | |
# while True: | |
# status = self.get_cleaned_data() | |
# if status != self.current_data: | |
# self.status_queue.put(status) | |
# self.current_data = status | |
# sleep(1) | |
def get_file_modified_time(self) -> float: | |
return os.path.getmtime(self.file_path) | |
def get_cargo_data(self): | |
"""Loads data from the JSON file and returns the data. | |
{ "timestamp":"2025-04-20T23:23:25Z", "event":"Cargo", "Vessel":"Ship", "Count":0, "Inventory":[ | |
] } | |
""" | |
# Check if file changed | |
if self.get_file_modified_time() == self.last_mod_time: | |
#logger.debug(f'Cargo.json mod timestamp {self.last_mod_time} unchanged.') | |
return self.current_data | |
# Read file | |
backoff = 1 | |
while True: | |
try: | |
with open(self.file_path, 'r') as file: | |
data = json.load(file) | |
break | |
except Exception as e: | |
logger.debug('An error occurred reading Cargo.json file. File may be open.') | |
sleep(backoff) | |
logger.debug('Attempting to re-read Cargo.json file after delay.') | |
backoff *= 2 | |
# Store data | |
self.current_data = data | |
self.last_mod_time = self.get_file_modified_time() | |
#logger.debug(f'Cargo.json mod timestamp {self.last_mod_time} updated.') | |
# print(json.dumps(data, indent=4)) | |
return data | |
def get_item(self, item_name: str) -> dict[any] | None: | |
""" Get details of one item. Returns the item detail as below, or None if item does not exist. | |
Will not trigger a read of the json file. | |
{ | |
"Name":"tritium", | |
"Count":356, | |
"Stolen":0 | |
} | |
@param item_name: The commodity name. | |
@return: Details on the commodity if in hold, else None. | |
""" | |
for good in self.current_data['Inventory']: | |
if good['Name'].upper() == item_name.upper(): | |
# print(json.dumps(good, indent=4)) | |
return good | |
return None | |
# Usage Example | |
if __name__ == "__main__": | |
cargo_parser = CargoParser() | |
while True: | |
cleaned_data = cargo_parser.get_cargo_data() | |
item = cargo_parser.get_item('Tritium') | |
time.sleep(1) | |
--- | |
File: /directinput.py | |
--- | |
# direct inputs | |
# source to this solution and code: | |
# http://stackoverflow.com/questions/14489013/simulate-python-keypresses-for-controlling-a-game | |
# http://www.gamespp.com/directx/directInputKeyboardScanCodes.html | |
import ctypes | |
import time | |
SendInput = ctypes.windll.user32.SendInput | |
# Listed are keyboard scan code constants, taken from dinput.h | |
SCANCODE = { | |
"Key_Escape": 1, | |
"Key_1": 2, | |
"Key_2": 3, | |
"Key_3": 4, | |
"Key_4": 5, | |
"Key_5": 6, | |
"Key_6": 7, | |
"Key_7": 8, | |
"Key_8": 9, | |
"Key_9": 10, | |
"Key_0": 11, | |
"Key_Minus": 12, | |
"Key_Equals": 13, | |
"Key_Backspace": 14, | |
"Key_Tab": 15, | |
"Key_Q": 16, | |
"Key_W": 17, | |
"Key_E": 18, | |
"Key_R": 19, | |
"Key_T": 20, | |
"Key_Y": 21, | |
"Key_U": 22, | |
"Key_I": 23, | |
"Key_O": 24, | |
"Key_P": 25, | |
"Key_LeftBracket": 26, | |
"Key_RightBracket": 27, | |
"Key_Enter": 28, | |
"Key_LeftControl": 29, | |
"Key_A": 30, | |
"Key_S": 31, | |
"Key_D": 32, | |
"Key_F": 33, | |
"Key_G": 34, | |
"Key_H": 35, | |
"Key_J": 36, | |
"Key_K": 37, | |
"Key_L": 38, | |
"Key_SemiColon": 39, | |
"Key_Apostrophe": 40, | |
"Key_Grave": 41, | |
"Key_LeftShift": 42, | |
"Key_BackSlash": 43, | |
"Key_Z": 44, | |
"Key_X": 45, | |
"Key_C": 46, | |
"Key_V": 47, | |
"Key_B": 48, | |
"Key_N": 49, | |
"Key_M": 50, | |
"Key_Comma": 51, | |
"Key_Period": 52, | |
"Key_Slash": 53, | |
"Key_RightShift": 54, | |
"Key_Numpad_Multiply": 55, | |
"Key_LeftAlt": 56, | |
"Key_Space": 57, | |
"Key_CapsLock": 58, | |
"Key_F1": 59, | |
"Key_F2": 60, | |
"Key_F3": 61, | |
"Key_F4": 62, | |
"Key_F5": 63, | |
"Key_F6": 64, | |
"Key_F7": 65, | |
"Key_F8": 66, | |
"Key_F9": 67, | |
"Key_F10": 68, | |
"Key_NumLock": 69, | |
"Key_ScrollLock": 70, | |
"Key_Numpad_7": 71, | |
"Key_Numpad_8": 72, | |
"Key_Numpad_9": 73, | |
"Key_Numpad_Subtract": 74, | |
"Key_Numpad_4": 75, | |
"Key_Numpad_5": 76, | |
"Key_Numpad_6": 77, | |
"Key_Numpad_Add": 78, | |
"Key_Numpad_1": 79, | |
"Key_Numpad_2": 80, | |
"Key_Numpad_3": 81, | |
"Key_Numpad_0": 82, | |
"Key_Numpad_Decimal": 83, | |
"??_84": 84, | |
"??_85": 85, | |
"??_86_duplicate_Key_BackSlash": 86, | |
"Key_F11": 87, | |
"Key_F12": 88, | |
"??_89": 89, | |
"??_90": 90, | |
"??_91": 91, | |
"??_92": 92, | |
"??_93": 93, | |
"??_94": 94, | |
"??_95": 95, | |
"??_96": 96, | |
"??_97": 97, | |
"??_98": 98, | |
"??_99": 99, | |
"??_100": 100, | |
"??_101": 101, | |
"??_102": 102, | |
"??_103": 103, | |
"??_104": 104, | |
"??_105": 105, | |
"??_106": 106, | |
"??_107": 107, | |
"??_108": 108, | |
"??_109": 109, | |
"??_110": 110, | |
"??_111": 111, | |
"??_112": 112, | |
"??_113": 113, | |
"??_114": 114, | |
"Key_ABNT_C1": 115, | |
"??_116": 116, | |
"??_117": 117, | |
"??_118": 118, | |
"??_119": 119, | |
"??_120": 120, | |
"??_121": 121, | |
"??_122": 122, | |
"??_123": 123, | |
"??_124": 124, | |
"??_125": 125, | |
"Key_ABNT_C2": 126, | |
"??_127": 127, | |
"??_128": 128, | |
"??_129": 129, | |
"??_130": 130, | |
"??_131": 131, | |
"??_132": 132, | |
"??_133": 133, | |
"??_134": 134, | |
"??_135": 135, | |
"??_136": 136, | |
"??_137": 137, | |
"??_138": 138, | |
"??_139": 139, | |
"??_140": 140, | |
"??_141": 141, | |
"??_142": 142, | |
"??_143": 143, | |
"Key_PrevTrack": 144, | |
"??_145": 145, | |
"??_146": 146, | |
"??_147": 147, | |
"??_148": 148, | |
"??_149": 149, | |
"??_150": 150, | |
"??_151": 151, | |
"??_152": 152, | |
"Key_NextTrack": 153, | |
"??_154": 154, | |
"??_155": 155, | |
"Key_Numpad_Enter": 156, | |
"Key_RightControl": 157, | |
"??_158": 158, | |
"??_159": 159, | |
"Key_Mute": 160, | |
"Key_Calculator": 161, | |
"Key_PlayPause": 162, | |
"??_163": 163, | |
"Key_MediaStop": 164, | |
"??_165": 165, | |
"??_166": 166, | |
"??_167": 167, | |
"??_168": 168, | |
"??_169": 169, | |
"??_170": 170, | |
"??_171": 171, | |
"??_172": 172, | |
"??_173": 173, | |
"Key_VolumeDown": 174, | |
"??_175": 175, | |
"Key_VolumeUp": 176, | |
"??_177": 177, | |
"Key_WebHome": 178, | |
"??_179": 179, | |
"??_180": 180, | |
"Key_Numpad_Divide": 181, | |
"??_182": 182, | |
"Key_SYSRQ": 183, | |
"Key_RightAlt": 184, | |
"??_185": 185, | |
"??_186": 186, | |
"??_187": 187, | |
"??_188": 188, | |
"??_189": 189, | |
"??_190": 190, | |
"??_191": 191, | |
"??_192": 192, | |
"??_193": 193, | |
"??_194": 194, | |
"??_195": 195, | |
"??_196": 196, | |
"Key_Pause": 197, | |
"??_198": 198, | |
"Key_Home": 199, | |
"Key_UpArrow": 200, | |
"Key_PageUp": 201, | |
"??_202": 202, | |
"Key_LeftArrow": 203, | |
"??_204": 204, | |
"Key_RightArrow": 205, | |
"??_206": 206, | |
"Key_End": 207, | |
"Key_DownArrow": 208, | |
"Key_PageDown": 209, | |
"Key_Insert": 210, | |
"Key_Delete": 211, | |
"??_212": 212, | |
"??_213_possibly_print_screen": 213, | |
"??_214": 214, | |
"??_215": 215, | |
"??_216": 216, | |
"??_217": 217, | |
"??_218": 218, | |
"??_219": 219, | |
"??_220": 220, | |
"Key_Apps": 221, | |
"Key_Power": 222, | |
"Key_Sleep": 223, | |
"??_224": 224, | |
"??_225": 225, | |
"??_226": 226, | |
"Key_Wake": 227, | |
"??_228": 228, | |
"Key_WebSearch": 229, | |
"Key_WebFavourites": 230, | |
"Key_WebRefresh": 231, | |
"Key_WebStop": 232, | |
"Key_WebForward": 233, | |
"Key_WebBack": 234, | |
"Key_MyComputer": 235, | |
"Key_Mail": 236, | |
"Key_MediaSelect": 237, | |
"??_238": 238, | |
"??_239": 239, | |
"??_240": 240, | |
"??_241": 241, | |
"??_242": 242, | |
"??_243": 243, | |
"??_244": 244, | |
"??_245": 245, | |
"??_246": 246, | |
"??_247": 247, | |
"??_248": 248, | |
"??_249": 249, | |
"??_250": 250, | |
"??_251": 251, | |
"??_252": 252, | |
"??_253": 253, | |
"??_254": 254 | |
} | |
# C struct redefinitions | |
PUL = ctypes.POINTER(ctypes.c_ulong) | |
class KeyBdInput(ctypes.Structure): | |
_fields_ = [("wVk", ctypes.c_ushort), | |
("wScan", ctypes.c_ushort), | |
("dwFlags", ctypes.c_ulong), | |
("time", ctypes.c_ulong), | |
("dwExtraInfo", PUL)] | |
class HardwareInput(ctypes.Structure): | |
_fields_ = [("uMsg", ctypes.c_ulong), | |
("wParamL", ctypes.c_short), | |
("wParamH", ctypes.c_ushort)] | |
class MouseInput(ctypes.Structure): | |
_fields_ = [("dx", ctypes.c_long), | |
("dy", ctypes.c_long), | |
("mouseData", ctypes.c_ulong), | |
("dwFlags", ctypes.c_ulong), | |
("time",ctypes.c_ulong), | |
("dwExtraInfo", PUL)] | |
class Input_I(ctypes.Union): | |
_fields_ = [("ki", KeyBdInput), | |
("mi", MouseInput), | |
("hi", HardwareInput)] | |
class Input(ctypes.Structure): | |
_fields_ = [("type", ctypes.c_ulong), | |
("ii", Input_I)] | |
# Actual Functions | |
def PressKey(hexKeyCode): | |
extra = ctypes.c_ulong(0) | |
ii_ = Input_I() | |
ii_.ki = KeyBdInput(0, hexKeyCode, 0x0008, 0, ctypes.pointer(extra)) | |
x = Input(ctypes.c_ulong(1), ii_) | |
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x)) | |
def ReleaseKey(hexKeyCode): | |
extra = ctypes.c_ulong(0) | |
ii_ = Input_I() | |
ii_.ki = KeyBdInput(0, hexKeyCode, 0x0008 | 0x0002, 0, ctypes.pointer(extra)) | |
x = Input(ctypes.c_ulong(1), ii_) | |
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x)) | |
--- | |
File: /ED_AP.py | |
--- | |
import math | |
import traceback | |
from math import atan, degrees | |
import random | |
from tkinter import messagebox | |
import cv2 | |
from simple_localization import LocalizationManager | |
from EDAP_EDMesg_Server import EDMesgServer | |
from EDAP_data import * | |
from EDGalaxyMap import EDGalaxyMap | |
from EDGraphicsSettings import EDGraphicsSettings | |
from EDShipControl import EDShipControl | |
from EDStationServicesInShip import EDStationServicesInShip | |
from EDSystemMap import EDSystemMap | |
from EDlogger import logging | |
import Image_Templates | |
import Screen | |
import Screen_Regions | |
from EDWayPoint import * | |
from EDJournal import * | |
from EDKeys import * | |
from EDafk_combat import AFK_Combat | |
from EDInternalStatusPanel import EDInternalStatusPanel | |
from NavRouteParser import NavRouteParser | |
from OCR import OCR | |
from Overlay import * | |
from StatusParser import StatusParser | |
from Voice import * | |
from Robigo import * | |
""" | |
File:EDAP.py EDAutopilot | |
Description: | |
Note: | |
Ideas taken from: https://github.com/skai2/EDAutopilot | |
Author: [email protected] | |
""" | |
# Exception class used to unroll the call tree to to stop execution | |
class EDAP_Interrupt(Exception): | |
pass | |
class EDAutopilot: | |
def __init__(self, cb, doThread=True): | |
# NOTE!!! When adding a new config value below, add the same after read_config() to set | |
# a default value or an error will occur reading the new value! | |
self.config = { | |
"DSSButton": "Primary", # if anything other than "Primary", it will use the Secondary Fire button for DSS | |
"JumpTries": 3, # | |
"NavAlignTries": 3, # | |
"RefuelThreshold": 65, # if fuel level get below this level, it will attempt refuel | |
"FuelThreasholdAbortAP": 10, # level at which AP will terminate, because we are not scooping well | |
"WaitForAutoDockTimer": 120, # After docking granted, wait this amount of time for us to get docked with autodocking | |
"SunBrightThreshold": 125, # The low level for brightness detection, range 0-255, want to mask out darker items | |
"FuelScoopTimeOut": 35, # number of second to wait for full tank, might mean we are not scooping well or got a small scooper | |
"DockingRetries": 30, # number of time to attempt docking | |
"HotKey_StartFSD": "home", # if going to use other keys, need to look at the python keyboard package | |
"HotKey_StartSC": "ins", # to determine other keynames, make sure these keys are not used in ED bindings | |
"HotKey_StartRobigo": "pgup", # | |
"HotKey_StopAllAssists": "end", | |
"Robigo_Single_Loop": False, # True means only 1 loop will executed and then terminate the Robigo, will not perform mission processing | |
"EnableRandomness": False, # add some additional random sleep times to avoid AP detection (0-3sec at specific locations) | |
"ActivateEliteEachKey": False, # Activate Elite window before each key or group of keys | |
"OverlayTextEnable": False, # Experimental at this stage | |
"OverlayTextYOffset": 400, # offset down the screen to start place overlay text | |
"OverlayTextXOffset": 50, # offset left the screen to start place overlay text | |
"OverlayTextFont": "Eurostyle", | |
"OverlayTextFontSize": 14, | |
"OverlayGraphicEnable": False, # not implemented yet | |
"DiscordWebhook": False, # discord not implemented yet | |
"DiscordWebhookURL": "", | |
"DiscordUserID": "", | |
"VoiceEnable": False, | |
"VoiceID": 1, # my Windows only have 3 defined (0-2) | |
"ElwScannerEnable": False, | |
"LogDEBUG": False, # enable for debug messages | |
"LogINFO": True, | |
"Enable_CV_View": 0, # Should CV View be enabled by default | |
"ShipConfigFile": None, # Ship config to load on start - deprecated | |
"TargetScale": 1.0, # Scaling of the target when a system is selected | |
"TCEDestinationFilepath": "C:\\TCE\\DUMP\\Destination.json", # Destination file for TCE | |
"AutomaticLogout": False, # Logout when we are done with the mission | |
"FCDepartureTime": 5.0, # Extra time to fly away from a Fleet Carrier | |
"Language": 'en', # Language (matching ./locales/xx.json file) | |
"EnableEDMesg": False, | |
"EDMesgActionsPort": 15570, | |
"EDMesgEventsPort": 15571, | |
} | |
# NOTE!!! When adding a new config value above, add the same after read_config() to set | |
# a default value or an error will occur reading the new value! | |
self.ship_configs = { | |
"Ship_Configs": {}, # Dictionary of ship types with additional settings | |
} | |
self._sc_sco_active_loop_thread = None | |
self._sc_sco_active_loop_enable = False | |
self.sc_sco_is_active = 0 | |
self._sc_sco_active_on_ls = 0 | |
self._single_waypoint_station = None | |
self._single_waypoint_system = None | |
self._prev_star_system = None | |
self.honk_thread = None | |
# used this to write the self.config table to the json file | |
# self.write_config(self.config) | |
cnf = self.read_config() | |
# if we read it then point to it, otherwise use the default table above | |
if cnf is not None: | |
if len(cnf) != len(self.config): | |
# If configs of different lengths, then a new parameter was added. | |
# self.write_config(self.config) | |
# Add default values for new entries | |
if 'SunBrightThreshold' not in cnf: | |
cnf['SunBrightThreshold'] = 125 | |
if 'TargetScale' not in cnf: | |
cnf['TargetScale'] = 1.0 | |
if 'TCEDestinationFilepath' not in cnf: | |
cnf['TCEDestinationFilepath'] = "C:\\TCE\\DUMP\\Destination.json" | |
if 'AutomaticLogout' not in cnf: | |
cnf['AutomaticLogout'] = False | |
if 'FCDepartureTime' not in cnf: | |
cnf['FCDepartureTime'] = 5.0 | |
if 'Language' not in cnf: | |
cnf['Language'] = 'en' | |
if 'EnableEDMesg' not in cnf: | |
cnf['EnableEDMesg'] = False | |
cnf['EDMesgActionsPort'] = 15570 | |
cnf['EDMesgEventsPort'] = 15571 | |
self.config = cnf | |
logger.debug("read AP json:"+str(cnf)) | |
else: | |
self.config = cnf | |
logger.debug("read AP json:"+str(cnf)) | |
else: | |
self.write_config(self.config) | |
# Load selected language | |
self.locale = LocalizationManager('locales', self.config['Language']) | |
shp_cnf = self.read_ship_configs() | |
# if we read it then point to it, otherwise use the default table above | |
if shp_cnf is not None: | |
if len(shp_cnf) != len(self.ship_configs): | |
# If configs of different lengths, then a new parameter was added. | |
# self.write_config(self.config) | |
# Add default values for new entries | |
if 'Ship_Configs' not in shp_cnf: | |
shp_cnf['Ship_Configs'] = dict() | |
self.ship_configs = shp_cnf | |
logger.debug("read Ships Config json:" + str(shp_cnf)) | |
else: | |
self.ship_configs = shp_cnf | |
logger.debug("read Ships Config json:" + str(shp_cnf)) | |
else: | |
self.write_ship_configs(self.ship_configs) | |
# config the voice interface | |
self.vce = Voice() | |
self.vce.v_enabled = self.config['VoiceEnable'] | |
self.vce.set_voice_id(self.config['VoiceID']) | |
self.vce.say("Welcome to Autopilot") | |
# set log level based on config input | |
if self.config['LogINFO']: | |
logger.setLevel(logging.INFO) | |
if self.config['LogDEBUG']: | |
logger.setLevel(logging.DEBUG) | |
# initialize all to false | |
self.fsd_assist_enabled = False | |
self.sc_assist_enabled = False | |
self.afk_combat_assist_enabled = False | |
self.waypoint_assist_enabled = False | |
self.robigo_assist_enabled = False | |
self.dss_assist_enabled = False | |
self.single_waypoint_enabled = False | |
# Create instance of each of the needed Classes | |
self.gfx_settings = EDGraphicsSettings() | |
self.scr = Screen.Screen(cb) | |
self.scr.scaleX = self.config['TargetScale'] | |
self.scr.scaleY = self.config['TargetScale'] | |
self.ocr = OCR(self.scr) | |
self.templ = Image_Templates.Image_Templates(self.scr.scaleX, self.scr.scaleY, self.scr.scaleX) | |
self.scrReg = Screen_Regions.Screen_Regions(self.scr, self.templ) | |
self.jn = EDJournal() | |
self.keys = EDKeys(cb) | |
self.keys.activate_window = self.config['ActivateEliteEachKey'] | |
self.afk_combat = AFK_Combat(self.keys, self.jn, self.vce) | |
self.waypoint = EDWayPoint(self, self.jn.ship_state()['odyssey']) | |
self.robigo = Robigo(self) | |
self.status = StatusParser() | |
self.nav_route = NavRouteParser() | |
self.ship_control = EDShipControl(self.scr, self.keys, cb) | |
self.internal_panel = EDInternalStatusPanel(self, self.scr, self.keys, cb) | |
self.galaxy_map = EDGalaxyMap(self, self.scr, self.keys, cb, self.jn.ship_state()['odyssey']) | |
self.system_map = EDSystemMap(self, self.scr, self.keys, cb, self.jn.ship_state()['odyssey']) | |
self.stn_svcs_in_ship = EDStationServicesInShip(self, self.scr, self.keys, cb) | |
self.mesg_server = EDMesgServer(self, cb) | |
self.mesg_server.actions_port = self.config['EDMesgActionsPort'] | |
self.mesg_server.events_port = self.config['EDMesgEventsPort'] | |
if self.config['EnableEDMesg']: | |
self.mesg_server.start_server() | |
# rate as ship dependent. Can be found on the outfitting page for the ship. However, it looks like supercruise | |
# has worse performance for these rates | |
# see: https://forums.frontier.co.uk/threads/supercruise-handling-of-ships.396845/ | |
# | |
# If you find that you are overshoot in pitch or roll, need to adjust these numbers. | |
# Algorithm will roll the vehicle for the nav point to be north or south and then pitch to get the nave point | |
# to center | |
self.compass_scale = 0.0 | |
self.yawrate = 8.0 | |
self.rollrate = 80.0 | |
self.pitchrate = 33.0 | |
self.sunpitchuptime = 0.0 | |
self.jump_cnt = 0 | |
self.total_dist_jumped = 0 | |
self.total_jumps = 0 | |
self.refuel_cnt = 0 | |
self.current_ship_type = None | |
self.gui_loaded = False | |
self.ap_ckb = cb | |
# Overlay vars | |
self.ap_state = "Idle" | |
self.fss_detected = "nothing found" | |
# Initialize the Overlay class | |
self.overlay = Overlay("", elite=1) | |
self.overlay.overlay_setfont(self.config['OverlayTextFont'], self.config['OverlayTextFontSize']) | |
self.overlay.overlay_set_pos(self.config['OverlayTextXOffset'], self.config['OverlayTextYOffset']) | |
# must be called after we initialized the objects above | |
self.update_overlay() | |
# debug window | |
self.cv_view = self.config['Enable_CV_View'] | |
self.cv_view_x = 10 | |
self.cv_view_y = 10 | |
#start the engine thread | |
self.terminate = False # terminate used by the thread to exit its loop | |
if doThread: | |
self.ap_thread = kthread.KThread(target=self.engine_loop, name="EDAutopilot") | |
self.ap_thread.start() | |
# Loads the configuration file | |
# | |
def read_config(self, fileName='./configs/AP.json'): | |
s = None | |
try: | |
with open(fileName, "r") as fp: | |
s = json.load(fp) | |
except Exception as e: | |
logger.warning("EDAPGui.py read_config error :"+str(e)) | |
return s | |
def update_config(self): | |
self.write_config(self.config) | |
def write_config(self, data, fileName='./configs/AP.json'): | |
try: | |
with open(fileName, "w") as fp: | |
json.dump(data, fp, indent=4) | |
except Exception as e: | |
logger.warning("EDAPGui.py write_config error:"+str(e)) | |
def read_ship_configs(self, filename='./configs/ship_configs.json'): | |
""" Read the user's ship configuration file.""" | |
s = None | |
try: | |
with open(filename, "r") as fp: | |
s = json.load(fp) | |
except Exception as e: | |
logger.warning("EDAPGui.py read_ship_configs error :"+str(e)) | |
return s | |
def update_ship_configs(self): | |
""" Update the user's ship configuration file.""" | |
# Check if a ship and not a suit (on foot) | |
if self.current_ship_type in ship_size_map: | |
self.ship_configs['Ship_Configs'][self.current_ship_type]['compass_scale'] = round(self.compass_scale, 4) | |
self.ship_configs['Ship_Configs'][self.current_ship_type]['PitchRate'] = self.pitchrate | |
self.ship_configs['Ship_Configs'][self.current_ship_type]['RollRate'] = self.rollrate | |
self.ship_configs['Ship_Configs'][self.current_ship_type]['YawRate'] = self.yawrate | |
self.ship_configs['Ship_Configs'][self.current_ship_type]['SunPitchUp+Time'] = self.sunpitchuptime | |
self.write_ship_configs(self.ship_configs) | |
def write_ship_configs(self, data, filename='./configs/ship_configs.json'): | |
""" Write the user's ship configuration file.""" | |
try: | |
with open(filename, "w") as fp: | |
json.dump(data, fp, indent=4) | |
except Exception as e: | |
logger.warning("EDAPGui.py write_ship_configs error:"+str(e)) | |
# draw the overlay data on the ED Window | |
# | |
def update_overlay(self): | |
if self.config['OverlayTextEnable']: | |
ap_mode = "Offline" | |
if self.fsd_assist_enabled == True: | |
ap_mode = "FSD Route Assist" | |
elif self.robigo_assist_enabled == True: | |
ap_mode = "Robigo Assist" | |
elif self.sc_assist_enabled == True: | |
ap_mode = "SC Assist" | |
elif self.waypoint_assist_enabled == True: | |
ap_mode = "Waypoint Assist" | |
elif self.afk_combat_assist_enabled == True: | |
ap_mode = "AFK Combat Assist" | |
elif self.dss_assist_enabled == True: | |
ap_mode = "DSS Assist" | |
ship_state = self.jn.ship_state()['status'] | |
if ship_state == None: | |
ship_state = '<init>' | |
sclass = self.jn.ship_state()['star_class'] | |
if sclass == None: | |
sclass = "<init>" | |
location = self.jn.ship_state()['location'] | |
if location == None: | |
location = "<init>" | |
self.overlay.overlay_text('1', "AP MODE: "+ap_mode, 1, 1, (136, 53, 0)) | |
self.overlay.overlay_text('2', "AP STATUS: "+self.ap_state, 2, 1, (136, 53, 0)) | |
self.overlay.overlay_text('3', "SHIP STATUS: "+ship_state, 3, 1, (136, 53, 0)) | |
self.overlay.overlay_text('4', "CURRENT SYSTEM: "+location+", "+sclass, 4, 1, (136, 53, 0)) | |
self.overlay.overlay_text('5', "JUMPS: {} of {}".format(self.jump_cnt, self.total_jumps), 5, 1, (136, 53, 0)) | |
if self.config["ElwScannerEnable"] == True: | |
self.overlay.overlay_text('6', "ELW SCANNER: "+self.fss_detected, 6, 1, (136, 53, 0)) | |
self.overlay.overlay_paint() | |
def update_ap_status(self, txt): | |
self.ap_state = txt | |
self.update_overlay() | |
self.ap_ckb('statusline', txt) | |
# draws the matching rectangle within the image | |
# | |
def draw_match_rect(self, img, pt1, pt2, color, thick): | |
wid = pt2[0]-pt1[0] | |
hgt = pt2[1]-pt1[1] | |
if wid < 20: | |
#cv2.rectangle(screen, pt, (pt[0] + compass_width, pt[1] + compass_height), (0,0,255), 2) | |
cv2.rectangle(img, pt1, pt2, color, thick) | |
else: | |
len_wid = wid/5 | |
len_hgt = hgt/5 | |
half_wid = wid/2 | |
half_hgt = hgt/2 | |
tic_len = thick-1 | |
# top | |
cv2.line(img, (int(pt1[0]), int(pt1[1])), (int(pt1[0]+len_wid), int(pt1[1])), color, thick) | |
cv2.line(img, (int(pt1[0]+(2*len_wid)), int(pt1[1])), (int(pt1[0]+(3*len_wid)), int(pt1[1])), color, 1) | |
cv2.line(img, (int(pt1[0]+(4*len_wid)), int(pt1[1])), (int(pt2[0]), int(pt1[1])), color, thick) | |
# top tic | |
cv2.line(img, (int(pt1[0]+half_wid), int(pt1[1])), (int(pt1[0]+half_wid), int(pt1[1])-tic_len), color, thick) | |
# bot | |
cv2.line(img, (int(pt1[0]), int(pt2[1])), (int(pt1[0]+len_wid), int(pt2[1])), color, thick) | |
cv2.line(img, (int(pt1[0]+(2*len_wid)), int(pt2[1])), (int(pt1[0]+(3*len_wid)), int(pt2[1])), color, 1) | |
cv2.line(img, (int(pt1[0]+(4*len_wid)), int(pt2[1])), (int(pt2[0]), int(pt2[1])), color, thick) | |
# bot tic | |
cv2.line(img, (int(pt1[0]+half_wid), int(pt2[1])), (int(pt1[0]+half_wid), int(pt2[1])+tic_len), color, thick) | |
# left | |
cv2.line(img, (int(pt1[0]), int(pt1[1])), (int(pt1[0]), int(pt1[1]+len_hgt)), color, thick) | |
cv2.line(img, (int(pt1[0]), int(pt1[1]+(2*len_hgt))), (int(pt1[0]), int(pt1[1]+(3*len_hgt))), color, 1) | |
cv2.line(img, (int(pt1[0]), int(pt1[1]+(4*len_hgt))), (int(pt1[0]), int(pt2[1])), color, thick) | |
# left tic | |
cv2.line(img, (int(pt1[0]), int(pt1[1]+half_hgt)), (int(pt1[0]-tic_len), int(pt1[1]+half_hgt)), color, thick) | |
# right | |
cv2.line(img, (int(pt2[0]), int(pt1[1])), (int(pt2[0]), int(pt1[1]+len_hgt)), color, thick) | |
cv2.line(img, (int(pt2[0]), int(pt1[1]+(2*len_hgt))), (int(pt2[0]), int(pt1[1]+(3*len_hgt))), color, 1) | |
cv2.line(img, (int(pt2[0]), int(pt1[1]+(4*len_hgt))), (int(pt2[0]), int(pt2[1])), color, thick) | |
# right tic | |
cv2.line(img, (int(pt2[0]), int(pt1[1]+half_hgt)), (int(pt2[0]+tic_len), int(pt1[1]+half_hgt)), color, thick) | |
def calibrate_region(self, range_low, range_high, range_step, threshold: float, reg_name: str, templ_name: str): | |
""" Find the best scale value in the given range of scales with the passed in threshold | |
@param reg_name: | |
@param range_low: | |
@param range_high: | |
@param range_step: | |
@param threshold: The minimum threshold to match (0.0 - 1.0) | |
@param templ_name: The region name i.i 'compass' or 'target' | |
@return: | |
""" | |
scale = 0 | |
max_pick = 0 | |
i = range_low | |
while i <= range_high: | |
self.scr.scaleX = float(i / 100) | |
self.scr.scaleY = self.scr.scaleX | |
# reload the templates with this scale value | |
self.templ.reload_templates(self.scr.scaleX, self.scr.scaleY, self.scr.scaleX) | |
# do image matching on the compass and the target | |
image, (minVal, maxVal, minLoc, maxLoc), match = self.scrReg.match_template_in_region(reg_name, templ_name) | |
border = 10 # border to prevent the box from interfering with future matches | |
reg_pos = self.scrReg.reg[reg_name]['rect'] | |
width = self.scrReg.templates.template[templ_name]['width'] + border + border | |
height = self.scrReg.templates.template[templ_name]['height'] + border + border | |
left = reg_pos[0] + maxLoc[0] - border | |
top = reg_pos[1] + maxLoc[1] - border | |
if maxVal > threshold and maxVal > max_pick: | |
# Draw box around region | |
self.overlay.overlay_rect(20, (left, top), (left + width, top + height), (0, 255, 0), 2) | |
self.overlay.overlay_floating_text(20, f'Match: {maxVal:5.4f}', left, top - 25, (0, 255, 0)) | |
else: | |
# Draw box around region | |
self.overlay.overlay_rect(21, (left, top), (left + width, top + height), (255, 0, 0), 2) | |
self.overlay.overlay_floating_text(21, f'Match: {maxVal:5.4f}', left, top - 25, (255, 0, 0)) | |
self.overlay.overlay_paint() | |
# Check the match percentage | |
if maxVal > threshold: | |
if maxVal > max_pick: | |
max_pick = maxVal | |
scale = i | |
#self.ap_ckb('log', 'Cal: Found match:' + f'{max_pick:5.4f}' + "% with scale:" + f'{self.scr.scaleX:5.4f}') | |
# Next range | |
i = i + range_step | |
# Leave the results for the user for a couple of seconds | |
sleep(2) | |
# Clean up screen | |
self.overlay.overlay_remove_rect(20) | |
self.overlay.overlay_remove_floating_text(20) | |
self.overlay.overlay_remove_rect(21) | |
self.overlay.overlay_remove_floating_text(21) | |
self.overlay.overlay_paint() | |
return scale, max_pick | |
def calibrate(self): | |
""" Routine to find the optimal scaling values for the template images. """ | |
msg = 'Select OK to begin Calibration. You must be in space and have a star system targeted in center screen.' | |
self.vce.say(msg) | |
ans = messagebox.askokcancel('Calibration', msg) | |
if not ans: | |
return | |
self.ap_ckb('log+vce', 'Calibration starting.') | |
self.set_focus_elite_window() | |
# Draw the target and compass regions on the screen | |
key = 'target' | |
targ_region = self.scrReg.reg[key] | |
self.overlay.overlay_rect1(key, targ_region['rect'], (0, 0, 255), 2) | |
self.overlay.overlay_floating_text(key, key, targ_region['rect'][0], targ_region['rect'][1], (0, 0, 255)) | |
self.overlay.overlay_paint() | |
# Calibrate system target | |
self.calibrate_target() | |
# Clean up | |
self.overlay.overlay_clear() | |
self.overlay.overlay_paint() | |
self.ap_ckb('log+vce', 'Calibration complete.') | |
def calibrate_compass(self): | |
""" Routine to find the optimal scaling values for the template images. """ | |
msg = 'Select OK to begin Calibration. You must be in space and have the compass visible.' | |
self.vce.say(msg) | |
ans = messagebox.askokcancel('Calibration', msg) | |
if not ans: | |
return | |
self.ap_ckb('log+vce', 'Calibration starting.') | |
self.set_focus_elite_window() | |
# Draw the target and compass regions on the screen | |
key = 'compass' | |
targ_region = self.scrReg.reg[key] | |
self.overlay.overlay_rect1(key, targ_region['rect'], (0, 0, 255), 2) | |
self.overlay.overlay_floating_text(key, key, targ_region['rect'][0], targ_region['rect'][1], (0, 0, 255)) | |
self.overlay.overlay_paint() | |
# Calibrate compass | |
self.calibrate_ship_compass() | |
# Clean up | |
self.overlay.overlay_clear() | |
self.overlay.overlay_paint() | |
self.ap_ckb('log+vce', 'Calibration complete.') | |
def calibrate_target(self): | |
""" Calibrate target """ | |
range_low = 30 | |
range_high = 200 | |
range_step = 1 | |
scale_max = 0 | |
max_val = 0 | |
# loop through the test twice. Once over the wide scaling range at 1% increments and once over a | |
# small scaling range at 0.1% increments. | |
# Find out which scale factor meets the highest threshold value. | |
for i in range(2): | |
threshold = 0.5 # Minimum match is constant. Result will always be the highest match. | |
scale, max_pick = self.calibrate_region(range_low, range_high, range_step, threshold, 'target', 'target') | |
if scale != 0: | |
scale_max = scale | |
max_val = max_pick | |
range_low = scale - 5 | |
range_high = scale + 5 | |
range_step = 0.1 | |
else: | |
break # no match found with threshold | |
# if we found a scaling factor that meets our criteria, then save it to the resolution.json file | |
if max_val != 0: | |
self.scr.scaleX = float(scale_max / 100) | |
self.scr.scaleY = self.scr.scaleX | |
self.ap_ckb('log', f'Target Cal: Best match: {max_val * 100:5.2f}% at scale: {self.scr.scaleX:5.4f}') | |
self.config['TargetScale'] = round(self.scr.scaleX, 4) | |
# self.scr.scales['Calibrated'] = [self.scr.scaleX, self.scr.scaleY] | |
self.scr.write_config( | |
data=None) # None means the writer will use its own scales variable which we modified | |
else: | |
self.ap_ckb('log', | |
f'Target Cal: Insufficient matching to meet reliability, max % match: {max_val * 100:5.2f}%') | |
# reload the templates with the new (or previous value) | |
self.templ.reload_templates(self.scr.scaleX, self.scr.scaleY, self.compass_scale) | |
def calibrate_ship_compass(self): | |
""" Calibrate Compass """ | |
range_low = 30 | |
range_high = 200 | |
range_step = 1 | |
scale_max = 0 | |
max_val = 0 | |
# loop through the test twice. Once over the wide scaling range at 1% increments and once over a | |
# small scaling range at 0.1% increments. | |
# Find out which scale factor meets the highest threshold value. | |
for i in range(2): | |
threshold = 0.5 # Minimum match is constant. Result will always be the highest match. | |
scale, max_pick = self.calibrate_region(range_low, range_high, range_step, threshold, 'compass','compass') | |
if scale != 0: | |
scale_max = scale | |
max_val = max_pick | |
range_low = scale - 5 | |
range_high = scale + 5 | |
range_step = 0.1 | |
else: | |
break # no match found with threshold | |
# if we found a scaling factor that meets our criteria, then save it to the resolution.json file | |
if max_val != 0: | |
c_scaleX = float(scale_max / 100) | |
self.ap_ckb('log', | |
f'Compass Cal: Max best match: {max_val * 100:5.2f}% with scale: {c_scaleX:5.4f}') | |
# Keep new value | |
self.compass_scale = c_scaleX | |
else: | |
self.ap_ckb('log', | |
f'Compass Cal: Insufficient matching to meet reliability, max % match: {max_val * 100:5.2f}%') | |
# reload the templates with the new (or previous value) | |
self.templ.reload_templates(self.scr.scaleX, self.scr.scaleY, self.compass_scale) | |
# Go into FSS, check to see if we have a signal waveform in the Earth, Water or Ammonia zone | |
# if so, announce finding and log the type of world found | |
# | |
def fss_detect_elw(self, scr_reg): | |
#open fss | |
self.keys.send('SetSpeedZero') | |
sleep(0.1) | |
self.keys.send('ExplorationFSSEnter') | |
sleep(2.5) | |
# look for a circle or signal in this region | |
elw_image, (minVal, maxVal, minLoc, maxLoc), match = scr_reg.match_template_in_region('fss', 'elw') | |
elw_sig_image, (minVal1, maxVal1, minLoc1, maxLoc1), match = scr_reg.match_template_in_image(elw_image, 'elw_sig') | |
# dvide the region in thirds. Earth, then Water, then Ammonio | |
wid_div3 = scr_reg.reg['fss']['width']/3 | |
# Exit out of FSS, we got the images we need to process | |
self.keys.send('ExplorationFSSQuit') | |
# Uncomment this to show on the ED Window where the region is define. Must run this file as an App, so uncomment out | |
# the main at the bottom of file | |
#self.overlay.overlay_rect('fss', (scr_reg.reg['fss']['rect'][0], scr_reg.reg['fss']['rect'][1]), | |
# (scr_reg.reg['fss']['rect'][2], scr_reg.reg['fss']['rect'][3]), (120, 255, 0),2) | |
#self.overlay.overlay_paint() | |
if self.cv_view: | |
elw_image_d = elw_image.copy() | |
elw_image_d = cv2.cvtColor(elw_image_d, cv2.COLOR_GRAY2RGB) | |
#self.draw_match_rect(elw_image_d, maxLoc, (maxLoc[0]+15,maxLoc[1]+15), (255,255,255), 1) | |
self.draw_match_rect(elw_image_d, maxLoc1, (maxLoc1[0]+15, maxLoc1[1]+25), (0, 0, 255), 1) | |
cv2.putText(elw_image_d, f'{maxVal1:5.2f}> .70', (1, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.30, (255, 255, 255), 1, cv2.LINE_AA) | |
cv2.imshow('fss', elw_image_d) | |
cv2.moveWindow('fss', self.cv_view_x, self.cv_view_y+100) | |
cv2.waitKey(30) | |
logger.info("elw detected:{0:6.2f} ".format(maxVal)+" sig:{0:6.2f}".format(maxVal1)) | |
# check if the circle or the signal meets probability number, if so, determine which type by its region | |
#if (maxVal > 0.65 or (maxVal1 > 0.60 and maxLoc1[1] < 30) ): | |
# only check for singal | |
if maxVal1 > 0.70 and maxLoc1[1] < 30: | |
if maxLoc1[0] < wid_div3: | |
sstr = "Earth" | |
elif maxLoc1[0] > (wid_div3*2): | |
sstr = "Water" | |
else: | |
sstr = "Ammonia" | |
# log the entry into the elw.txt file | |
f = open("elw.txt", 'a') | |
f.write(self.jn.ship_state()["location"]+", Type: "+sstr+ | |
", Probabilty: {0:3.0f}% ".format((maxVal1*100))+ | |
", Date: "+str(datetime.now())+str("\n")) | |
f.close() | |
self.vce.say(sstr+" like world detected ") | |
self.fss_detected = sstr+" like world detected " | |
logger.info(sstr+" world at: "+str(self.jn.ship_state()["location"])) | |
else: | |
self.fss_detected = "nothing found" | |
self.keys.send('SetSpeed100') | |
return | |
def have_destination(self, scr_reg) -> bool: | |
""" Check to see if the compass is on the screen. """ | |
icompass_image, (minVal, maxVal, minLoc, maxLoc), match = scr_reg.match_template_in_region('compass', 'compass') | |
logger.debug("has_destination:"+str(maxVal)) | |
# need > x in the match to say we do have a destination | |
if maxVal < scr_reg.compass_match_thresh: | |
return False | |
else: | |
return True | |
def interdiction_check(self) -> bool: | |
""" Checks if we are being interdicted. This can occur in SC and maybe in system jump by Thargoids | |
(needs to be verified). Returns False if not interdicted, True after interdiction is detected and we | |
get away. Use return result to determine the next action (continue, or do something else). | |
""" | |
# Return if we are not being interdicted. | |
if not self.status.get_flag(FlagsBeingInterdicted): | |
return False | |
# Interdiction detected. | |
self.vce.say("Danger. Interdiction detected.") | |
self.ap_ckb('log', 'Interdiction detected.') | |
# Keep setting speed to zero to submit while in supercruise or system jump. | |
while self.status.get_flag(FlagsSupercruise) or self.status.get_flag2(Flags2FsdHyperdriveCharging): | |
self.keys.send('SetSpeedZero') # Submit. | |
sleep(0.5) | |
# Set speed to 100%. | |
self.keys.send('SetSpeed100') | |
# Wait for cooldown to start. | |
self.status.wait_for_flag_on(FlagsFsdCooldown) | |
# Boost while waiting for cooldown to complete. | |
while not self.status.wait_for_flag_off(FlagsFsdCooldown, timeout=1): | |
self.keys.send('UseBoostJuice') | |
# Cooldown over, get us out of here. | |
self.keys.send('Supercruise') | |
# Start SCO monitoring | |
self.start_sco_monitoring() | |
# Wait for jump to supercruise, keep boosting. | |
while not self.status.get_flag(FlagsFsdJump): | |
self.keys.send('UseBoostJuice') | |
sleep(1) | |
# Update journal flag. | |
self.jn.ship_state()['interdicted'] = False # reset flag | |
return True | |
def get_nav_offset(self, scr_reg): | |
""" Determine the x,y offset from center of the compass of the nav point. | |
Returns the x,y,z value as x,y in degrees (-90 to 90) and z as 1 or -1. | |
{'roll': r, 'pit': p, 'yaw': y} | |
Where 'roll' is: | |
-180deg (6 o'oclock anticlockwise) to | |
0deg (12 o'clock) to | |
180deg (6 o'oclock clockwise) | |
""" | |
icompass_image, (minVal, maxVal, minLoc, maxLoc), match = ( | |
scr_reg.match_template_in_region('compass', 'compass')) | |
pt = maxLoc | |
# get wid/hgt of templates | |
c_wid = scr_reg.templates.template['compass']['width'] | |
c_hgt = scr_reg.templates.template['compass']['height'] | |
wid = scr_reg.templates.template['navpoint']['width'] | |
hgt = scr_reg.templates.template['navpoint']['height'] | |
# cut out the compass from the region | |
pad = 5 | |
compass_image = icompass_image[abs(pt[1]-pad): pt[1]+c_hgt+pad, abs(pt[0]-pad): pt[0]+c_wid+pad].copy() | |
# find the nav point within the compass box | |
navpt_image, (n_minVal, n_maxVal, n_minLoc, n_maxLoc), match = ( | |
scr_reg.match_template_in_image(compass_image, 'navpoint')) | |
n_pt = n_maxLoc | |
compass_x_min = pad | |
compass_x_max = c_wid + pad - wid | |
compass_y_min = pad | |
compass_y_max = c_hgt + pad - hgt | |
if n_maxVal < scr_reg.navpoint_match_thresh: | |
final_z_pct = -1.0 # Behind | |
# find the nav point within the compass box using the -behind template | |
navpt_image, (n_minVal, n_maxVal, n_minLoc, n_maxLoc), match = ( | |
scr_reg.match_template_in_image(compass_image, 'navpoint-behind')) | |
n_pt = n_maxLoc | |
else: | |
final_z_pct = 1.0 # Ahead | |
# Continue calc | |
final_x_pct = 2*(((n_pt[0]-compass_x_min)/(compass_x_max-compass_x_min))-0.5) # X as percent (-1.0 to 1.0, 0.0 in the center) | |
final_x_pct = max(min(final_x_pct, 1.0), -1.0) | |
final_y_pct = -2*(((n_pt[1]-compass_y_min)/(compass_y_max-compass_y_min))-0.5) # Y as percent (-1.0 to 1.0, 0.0 in the center) | |
final_y_pct = max(min(final_y_pct, 1.0), -1.0) | |
# Calc angle in degrees starting at 0 deg at 12 o'clock and increasing clockwise | |
# so 3 o'clock is +90° and 9 o'clock is -90°. | |
final_roll_deg = 0.0 | |
if final_x_pct > 0.0: | |
final_roll_deg = 90 - degrees(atan(final_y_pct/final_x_pct)) | |
elif final_x_pct < 0.0: | |
final_roll_deg = -90 - degrees(atan(final_y_pct/final_x_pct)) | |
# 'longitudinal' radius of compass at given 'latitude' | |
lng_rad_at_lat = math.cos(math.asin(final_y_pct)) | |
lng_rad_at_lat = max(lng_rad_at_lat, 0.001) # Prevent div by zero | |
# 'Latitudinal' radius of compass at given 'longitude' | |
lat_rad_at_lng = math.sin(math.acos(final_x_pct)) | |
lat_rad_at_lng = max(lat_rad_at_lng, 0.001) # Prevent div by zero | |
# Pitch and yaw as a % of the max as defined by the compass circle | |
pit_pct = max(min(final_y_pct/lat_rad_at_lng, 1.0), -1.0) | |
yaw_pct = max(min(final_x_pct/lng_rad_at_lat, 1.0), -1.0) | |
if final_z_pct > 0: | |
final_pit_deg = (-1 * degrees(math.acos(pit_pct))) + 90 # Y in deg (-90.0 to 90.0, 0.0 in the center) | |
final_yaw_deg = (-1 * degrees(math.acos(yaw_pct))) + 90 # X in deg (-90.0 to 90.0, 0.0 in the center) | |
else: | |
if final_y_pct > 0: | |
final_pit_deg = degrees(math.acos(pit_pct)) + 90 # Y in deg (-90.0 to 90.0, 0.0 in the center) | |
else: | |
final_pit_deg = degrees(math.acos(pit_pct)) - 270 # Y in deg (-90.0 to 90.0, 0.0 in the center) | |
if final_x_pct > 0: | |
final_yaw_deg = degrees(math.acos(yaw_pct)) + 90 # X in deg (-90.0 to 90.0, 0.0 in the center) | |
else: | |
final_yaw_deg = degrees(math.acos(yaw_pct)) - 270 # X in deg (-90.0 to 90.0, 0.0 in the center) | |
result = {'x': round(final_x_pct, 2), 'y': round(final_y_pct, 2), 'z': round(final_z_pct, 2), | |
'roll': round(final_roll_deg, 2), 'pit': round(final_pit_deg, 2), 'yaw': round(final_yaw_deg, 2)} | |
if self.cv_view: | |
icompass_image_d = cv2.cvtColor(icompass_image, cv2.COLOR_GRAY2RGB) | |
self.draw_match_rect(icompass_image_d, pt, (pt[0]+c_wid, pt[1]+c_hgt), (0, 0, 255), 2) | |
#cv2.rectangle(icompass_image_display, pt, (pt[0]+c_wid, pt[1]+c_hgt), (0, 0, 255), 2) | |
#self.draw_match_rect(compass_image, n_pt, (n_pt[0] + wid, n_pt[1] + hgt), (255,255,255), 2) | |
self.draw_match_rect(icompass_image_d, (pt[0]+n_pt[0]-pad, pt[1]+n_pt[1]-pad), (pt[0]+n_pt[0]+wid-pad, pt[1]+n_pt[1]+hgt-pad), (0, 255, 0), 1) | |
#cv2.rectangle(icompass_image_display, (pt[0]+n_pt[0]-pad, pt[1]+n_pt[1]-pad), (pt[0]+n_pt[0] + wid-pad, pt[1]+n_pt[1] + hgt-pad), (0, 0, 255), 2) | |
# dim = (int(destination_width/3), int(destination_height/3)) | |
# img = cv2.resize(dst_image, dim, interpolation =cv2.INTER_AREA) | |
icompass_image_d = cv2.rectangle(icompass_image_d, (0, 0), (1000, 60), (0, 0, 0), -1) | |
cv2.putText(icompass_image_d, f'Compass: {maxVal:5.4f} > {scr_reg.compass_match_thresh:5.2f}', (1, 10), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA) | |
cv2.putText(icompass_image_d, f'Nav Point: {n_maxVal:5.4f} > {scr_reg.navpoint_match_thresh:5.2f}', (1, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA) | |
#cv2.putText(icompass_image_d, f'Result: {result}', (1, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA) | |
cv2.putText(icompass_image_d, f'x: {final_x_pct:5.2f} y: {final_y_pct:5.2f} z: {final_z_pct:5.2f}', (1, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA) | |
cv2.putText(icompass_image_d, f'r: {final_roll_deg:5.2f}deg p: {final_pit_deg:5.2f}deg y: {final_yaw_deg:5.2f}deg', (1, 55), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA) | |
cv2.imshow('compass', icompass_image_d) | |
cv2.moveWindow('compass', self.cv_view_x - 400, self.cv_view_y + 600) | |
cv2.waitKey(30) | |
return result | |
def is_destination_occluded(self, scr_reg) -> bool: | |
""" Looks to see if the 'dashed' line of the target is present indicating the target | |
is occluded by the planet. | |
@param scr_reg: The screen region to check. | |
@return: True if target occluded (meets threshold), else False. | |
""" | |
dst_image, (minVal, maxVal, minLoc, maxLoc), match = scr_reg.match_template_in_region('target_occluded', 'target_occluded', inv_col=False) | |
pt = maxLoc | |
if self.cv_view: | |
dst_image_d = cv2.cvtColor(dst_image, cv2.COLOR_GRAY2RGB) | |
destination_width = scr_reg.reg['target']['width'] | |
destination_height = scr_reg.reg['target']['height'] | |
width = scr_reg.templates.template['target_occluded']['width'] | |
height = scr_reg.templates.template['target_occluded']['height'] | |
try: | |
self.draw_match_rect(dst_image_d, pt, (pt[0]+width, pt[1]+height), (0, 0, 255), 2) | |
dim = (int(destination_width/2), int(destination_height/2)) | |
img = cv2.resize(dst_image_d, dim, interpolation=cv2.INTER_AREA) | |
img = cv2.rectangle(img, (0, 0), (1000, 25), (0, 0, 0), -1) | |
cv2.putText(img, f'{maxVal:5.4f} > {scr_reg.target_occluded_thresh:5.2f}', (1, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA) | |
cv2.imshow('occluded', img) | |
cv2.moveWindow('occluded', self.cv_view_x, self.cv_view_y+650) | |
except Exception as e: | |
print("exception in getdest: "+str(e)) | |
cv2.waitKey(30) | |
if maxVal > scr_reg.target_occluded_thresh: | |
logger.debug(f"Target is occluded ({maxVal:5.4f} > {scr_reg.target_occluded_thresh:5.2f})") | |
return True | |
else: | |
#logger.debug(f"Target is not occluded ({maxVal:5.4f} < {scr_reg.target_occluded_thresh:5.2f})") | |
return False | |
def get_destination_offset(self, scr_reg): | |
""" Determine how far off we are from the target being in the middle of the screen | |
(in this case the specified region). """ | |
dst_image, (minVal, maxVal, minLoc, maxLoc), match = scr_reg.match_template_in_region('target', 'target') | |
pt = maxLoc | |
destination_width = scr_reg.reg['target']['width'] | |
destination_height = scr_reg.reg['target']['height'] | |
width = scr_reg.templates.template['target']['width'] | |
height = scr_reg.templates.template['target']['height'] | |
# need some fug numbers since our template is not symetric to determine center | |
final_x = ((pt[0]+((1/2)*width))-((1/2)*destination_width))-7 | |
final_y = (((1/2)*destination_height)-(pt[1]+((1/2)*height)))+22 | |
# print("get dest, final:" + str(final_x)+ " "+str(final_y)) | |
# print(destination_width, destination_height, width, height) | |
# print(maxLoc) | |
if self.cv_view: | |
dst_image_d = cv2.cvtColor(dst_image, cv2.COLOR_GRAY2RGB) | |
try: | |
self.draw_match_rect(dst_image_d, pt, (pt[0]+width, pt[1]+height), (0, 0, 255), 2) | |
dim = (int(destination_width/2), int(destination_height/2)) | |
img = cv2.resize(dst_image_d, dim, interpolation=cv2.INTER_AREA) | |
img = cv2.rectangle(img, (0, 0), (1000, 25), (0, 0, 0), -1) | |
cv2.putText(img, f'{maxVal:5.4f} > {scr_reg.target_thresh:5.2f}', (1, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA) | |
cv2.imshow('target', img) | |
#cv2.imshow('tt', scr_reg.templates.template['target']['image']) | |
cv2.moveWindow('target', self.cv_view_x, self.cv_view_y+425) | |
except Exception as e: | |
print("exception in getdest: "+str(e)) | |
cv2.waitKey(30) | |
#print (maxVal) | |
# must be > x to have solid hit, otherwise we are facing wrong way (empty circle) | |
if maxVal < scr_reg.target_thresh: | |
result = None | |
else: | |
result = {'x': final_x, 'y': final_y} | |
return result | |
def sc_disengage_label_up(self, scr_reg) -> bool: | |
""" look for messages like "PRESS [J] TO DISENGAGE" or "SUPERCRUISE OVERCHARGE ACTIVE", | |
if in this region then return true. | |
The aim of this function is to return that a message is there, and then use OCR to determine | |
what the message is. This will only use the high CPU usage OCR when necessary.""" | |
dis_image, (minVal, maxVal, minLoc, maxLoc), match = scr_reg.match_template_in_region('disengage', 'disengage') | |
pt = maxLoc | |
width = scr_reg.templates.template['disengage']['width'] | |
height = scr_reg.templates.template['disengage']['height'] | |
if self.cv_view: | |
self.draw_match_rect(dis_image, pt, (pt[0] + width, pt[1] + height), (0,255,0), 2) | |
dis_image = cv2.rectangle(dis_image, (0, 0), (1000, 25), (0, 0, 0), -1) | |
cv2.putText(dis_image, f'{maxVal:5.4f} > {scr_reg.disengage_thresh}', (1, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA) | |
cv2.imshow('sc_disengage_label_up', dis_image) | |
cv2.moveWindow('sc_disengage_label_up', self.cv_view_x-460,self.cv_view_y+575) | |
cv2.waitKey(1) | |
if maxVal > scr_reg.disengage_thresh: | |
return True | |
else: | |
return False | |
def sc_disengage(self, scr_reg) -> bool: | |
""" DEPRECATED - Replaced with 'sc_disengage_label_up' and 'sc_disengage_active' using OCR. | |
look for the "PRESS [J] TO DISENGAGE" image, if in this region then return true | |
""" | |
dis_image, (minVal, maxVal, minLoc, maxLoc), match = scr_reg.match_template_in_region('disengage', 'disengage') | |
pt = maxLoc | |
width = scr_reg.templates.template['disengage']['width'] | |
height = scr_reg.templates.template['disengage']['height'] | |
if self.cv_view: | |
self.draw_match_rect(dis_image, pt, (pt[0] + width, pt[1] + height), (0,255,0), 2) | |
dis_image = cv2.rectangle(dis_image, (0, 0), (1000, 25), (0, 0, 0), -1) | |
cv2.putText(dis_image, f'{maxVal:5.4f} > {scr_reg.disengage_thresh}', (1, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA) | |
cv2.imshow('disengage', dis_image) | |
cv2.moveWindow('disengage', self.cv_view_x-460,self.cv_view_y+575) | |
cv2.waitKey(1) | |
#logger.debug("Disenage = "+str(maxVal)) | |
if maxVal > scr_reg.disengage_thresh: | |
logger.info("'PRESS [] TO DISENGAGE' detected. Disengaging Supercruise") | |
self.vce.say("Disengaging Supercruise") | |
return True | |
else: | |
return False | |
def sc_disengage_active(self, scr_reg) -> bool: | |
""" look for the "SUPERCRUISE OVERCHARGE ACTIVE" text using OCR, if in this region then return true. """ | |
image = self.scr.get_screen_region(scr_reg.reg['disengage']['rect']) | |
# TODO delete this line when COLOR_RGB2BGR is removed from get_screen() | |
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) | |
mask = scr_reg.capture_region_filtered(self.scr, 'disengage') | |
masked_image = cv2.bitwise_and(image, image, mask=mask) | |
image = masked_image | |
# OCR the selected item | |
sim_match = 0.35 # Similarity match 0.0 - 1.0 for 0% - 100%) | |
sim = 0.0 | |
ocr_textlist = self.ocr.image_simple_ocr(image) | |
if ocr_textlist is not None: | |
sim = self.ocr.string_similarity(self.locale["PRESS_TO_DISENGAGE_MSG"], str(ocr_textlist)) | |
logger.info(f"Disengage similarity with {str(ocr_textlist)} is {sim}") | |
if self.cv_view: | |
image = cv2.rectangle(image, (0, 0), (1000, 30), (0, 0, 0), -1) | |
cv2.putText(image, f'Text: {str(ocr_textlist)}', (1, 10), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA) | |
cv2.putText(image, f'Similarity: {sim:5.4f} > {sim_match}', (1, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA) | |
cv2.imshow('disengage2', image) | |
cv2.moveWindow('disengage2', self.cv_view_x - 460, self.cv_view_y + 650) | |
cv2.waitKey(30) | |
if sim > sim_match: | |
logger.info("'PRESS [] TO DISENGAGE' detected. Disengaging Supercruise") | |
#cv2.imwrite(f'test/disengage.png', image) | |
return True | |
return False | |
def start_sco_monitoring(self): | |
""" Start Supercruise Overcharge Monitoring. This starts a parallel thread used to detect SCO | |
until stop_sco_monitoring if called. """ | |
self._sc_sco_active_loop_enable = True | |
if self._sc_sco_active_loop_thread is None or not self._sc_sco_active_loop_thread.is_alive(): | |
self._sc_sco_active_loop_thread = threading.Thread(target=self._sc_sco_active_loop, daemon=True) | |
self._sc_sco_active_loop_thread.start() | |
def stop_sco_monitoring(self): | |
""" Stop Supercruise Overcharge Monitoring. """ | |
self._sc_sco_active_loop_enable = False | |
def _sc_sco_active_loop(self): | |
""" A loop to determine is Supercruise Overcharge is active. | |
This runs on a separate thread monitoring the status in the background. """ | |
while self._sc_sco_active_loop_enable: | |
# Try to determine if the disengage/sco text is there | |
sc_sco_is_active_ls = self.sc_sco_is_active | |
# Check if SCO active in flags | |
self.sc_sco_is_active = self.status.get_flag2(Flags2FsdScoActive) | |
if self.sc_sco_is_active and not sc_sco_is_active_ls: | |
self.ap_ckb('log+vce', "Supercruise Overcharge activated") | |
if sc_sco_is_active_ls and not self.sc_sco_is_active: | |
self.ap_ckb('log+vce', "Supercruise Overcharge deactivated") | |
# Protection if SCO is active | |
if self.sc_sco_is_active: | |
if self.status.get_flag(FlagsOverHeating): | |
logger.info("SCO Aborting, overheating") | |
self.ap_ckb('log+vce', "SCO Aborting, overheating") | |
self.keys.send('UseBoostJuice') | |
elif self.status.get_flag(FlagsLowFuel): | |
logger.info("SCO Aborting, < 25% fuel") | |
self.ap_ckb('log+vce', "SCO Aborting, < 25% fuel") | |
self.keys.send('UseBoostJuice') | |
elif self.jn.ship_state()['fuel_percent'] < self.config['FuelThreasholdAbortAP']: | |
logger.info("SCO Aborting, < users low fuel threshold") | |
self.ap_ckb('log+vce', "SCO Aborting, < users low fuel threshold") | |
self.keys.send('UseBoostJuice') | |
# Check again in a bit | |
sleep(1) | |
def undock(self): | |
""" Performs menu action to undock from Station """ | |
# Go to cockpit view | |
self.ship_control.goto_cockpit_view() | |
# Now we are on initial menu, we go up to top (which is Refuel) | |
self.keys.send('UI_Up', repeat=3) | |
# down to Auto Undock and Select it... | |
self.keys.send('UI_Down') | |
self.keys.send('UI_Down') | |
self.keys.send('UI_Select') | |
self.keys.send('SetSpeedZero', repeat=2) | |
# Performs left menu ops to request docking | |
def request_docking(self, toCONTACT): | |
""" Request docking from Nav Panel. """ | |
self.keys.send('UI_Back', repeat=10) | |
self.keys.send('HeadLookReset') | |
self.keys.send('UIFocus', state=1) | |
self.keys.send('UI_Left') | |
self.keys.send('UIFocus', state=0) | |
sleep(0.5) | |
# we start with the Left Panel having "NAVIGATION" highlighted, we then need to right | |
# right twice to "CONTACTS". Notice of a FSD run, the LEFT panel is reset to "NAVIGATION" | |
# otherwise it is on the last tab you selected. Thus must start AP with "NAVIGATION" selected | |
if (toCONTACT == 1): | |
self.keys.send('CycleNextPanel', hold=0.2) | |
sleep(0.2) | |
self.keys.send('CycleNextPanel', hold=0.2) | |
# On the CONTACT TAB, go to top selection, do this 4 seconds to ensure at top | |
# then go right, which will be "REQUEST DOCKING" and select it | |
self.keys.send('UI_Up', hold=4) | |
self.keys.send('UI_Right') | |
self.keys.send('UI_Select') | |
sleep(0.3) | |
self.keys.send('UI_Back') | |
self.keys.send('HeadLookReset') | |
def dock(self): | |
""" Docking sequence. Assumes in normal space, will get closer to the Station | |
then zero the velocity and execute menu commands to request docking, when granted | |
will wait a configurable time for dock. Perform Refueling and Repair. | |
""" | |
# if not in normal space, give a few more sections as at times it will take a little bit | |
if self.jn.ship_state()['status'] != "in_space": | |
sleep(3) # sleep a little longer | |
if self.jn.ship_state()['status'] != "in_space": | |
logger.error('In dock(), after wait, but still not in_space') | |
sleep(5) # wait 5 seconds to get to 7.5km to request docking | |
self.keys.send('SetSpeed50') | |
if self.jn.ship_state()['status'] != "in_space": | |
self.keys.send('SetSpeedZero') | |
logger.error('In dock(), after long wait, but still not in_space') | |
raise Exception('Docking failed (not in space)') | |
sleep(12) | |
# At this point (of sleep()) we should be < 7.5km from the station. Go 0 speed | |
# if we get docking granted ED's docking computer will take over | |
self.keys.send('SetSpeedZero', repeat=2) | |
self.ap_ckb('log+vce', "Initiating Docking Procedure") | |
self.request_docking(1) | |
sleep(1) | |
tries = self.config['DockingRetries'] | |
granted = False | |
if self.jn.ship_state()['status'] == "dockinggranted": | |
granted = True | |
# Go back to navigation tab | |
self.request_docking_cleanup() | |
else: | |
for i in range(tries): | |
if self.jn.ship_state()['no_dock_reason'] == "Distance": | |
self.keys.send('SetSpeed50') | |
sleep(5) | |
self.keys.send('SetSpeedZero', repeat=2) | |
self.request_docking(0) | |
self.keys.send('SetSpeedZero', repeat=2) | |
sleep(1.5) | |
if self.jn.ship_state()['status'] == "dockinggranted": | |
granted = True | |
# Go back to navigation tab | |
self.request_docking_cleanup() | |
break | |
if self.jn.ship_state()['status'] == "dockingdenied": | |
pass | |
if not granted: | |
self.ap_ckb('log', 'Docking denied: '+str(self.jn.ship_state()['no_dock_reason'])) | |
logger.warning('Did not get docking authorization, reason:'+str(self.jn.ship_state()['no_dock_reason'])) | |
raise Exception('Docking failed (Did not get docking authorization)') | |
else: | |
# allow auto dock to take over | |
for i in range(self.config['WaitForAutoDockTimer']): | |
sleep(1) | |
if self.jn.ship_state()['status'] == "in_station": | |
# go to top item, select (which should be refuel) | |
self.keys.send('UI_Up', hold=3) | |
self.keys.send('UI_Select') # Refuel | |
sleep(0.5) | |
self.keys.send('UI_Right') # Repair | |
self.keys.send('UI_Select') | |
sleep(0.5) | |
self.keys.send('UI_Right') # Ammo | |
self.keys.send('UI_Select') | |
sleep(0.5) | |
self.keys.send("UI_Left", repeat=2) # back to fuel | |
return | |
self.ap_ckb('log', 'Auto dock timer timed out.') | |
logger.warning('Auto dock timer timed out. Aborting Docking.') | |
raise Exception('Docking failed (Auto dock timer timed out)') | |
def request_docking_cleanup(self): | |
""" After request docking, go back to NAVIGATION tab in Nav Panel from the CONTACTS tab. """ | |
self.keys.send('UI_Back', repeat=10) | |
self.keys.send('HeadLookReset') | |
self.keys.send('UIFocus', state=1) | |
self.keys.send('UI_Left') | |
self.keys.send('UIFocus', state=0) | |
sleep(0.5) | |
self.keys.send('CycleNextPanel', hold=0.2) # STATS tab | |
sleep(0.2) | |
self.keys.send('CycleNextPanel', hold=0.2) # NAVIGATION tab | |
sleep(0.3) | |
self.keys.send('UI_Back') | |
self.keys.send('HeadLookReset') | |
def is_sun_dead_ahead(self, scr_reg): | |
return scr_reg.sun_percent(scr_reg.screen) > 5 | |
# use to orient the ship to not be pointing right at the Sun | |
# Checks brightness in the region in front of us, if brightness exceeds a threshold | |
# then will pitch up until below threshold. | |
# | |
def sun_avoid(self, scr_reg): | |
logger.debug('align= avoid sun') | |
sleep(0.5) | |
# close to core the 'sky' is very bright with close stars, if we are pitch due to a non-scoopable star | |
# which is dull red, the star field is 'brighter' than the sun, so our sun avoidance could pitch up | |
# endlessly. So we will have a fail_safe_timeout to kick us out of pitch up if we've pitch past 110 degrees, but | |
# we'll add 3 more second for pad in case the user has a higher pitch rate than the vehicle can do | |
fail_safe_timeout = (120/self.pitchrate)+3 | |
starttime = time.time() | |
# if sun in front of us, then keep pitching up until it is below us | |
while self.is_sun_dead_ahead(scr_reg): | |
self.keys.send('PitchUpButton', state=1) | |
# check if we are being interdicted | |
interdicted = self.interdiction_check() | |
if interdicted: | |
# Continue journey after interdiction | |
self.keys.send('SetSpeedZero') | |
# if we are pitching more than N seconds break, may be in high density area star area (close to core) | |
if ((time.time()-starttime) > fail_safe_timeout): | |
logger.debug('sun avoid failsafe timeout') | |
print("sun avoid failsafe timeout") | |
break | |
sleep(0.35) # up slightly so not to overheat when scooping | |
# Some ships heat up too much and need pitch up a little further | |
if self.sunpitchuptime > 0.0: | |
sleep(self.sunpitchuptime) | |
self.keys.send('PitchUpButton', state=0) | |
# Some ships run cool so need to pitch down a little | |
if self.sunpitchuptime < 0.0: | |
self.keys.send('PitchDownButton', state=1) | |
sleep(-1.0 * self.sunpitchuptime) | |
self.keys.send('PitchDownButton', state=0) | |
def nav_align(self, scr_reg): | |
""" Use the compass to find the nav point position. Will then perform rotation and pitching | |
to put the nav point in the middle of the compass, i.e. target right in front of us """ | |
close = 10 # in degrees | |
if not (self.jn.ship_state()['status'] == 'in_supercruise' or self.jn.ship_state()['status'] == 'in_space'): | |
logger.error('align=err1, nav_align not in super or space') | |
raise Exception('nav_align not in super or space') | |
self.ap_ckb('log+vce', 'Compass Align') | |
# try multiple times to get aligned. If the sun is shining on console, this it will be hard to match | |
# the vehicle should be positioned with the sun below us via the sun_avoid() routine after a jump | |
for ii in range(self.config['NavAlignTries']): | |
off = self.get_nav_offset(scr_reg) | |
if abs(off['yaw']) < close and abs(off['pit']) < close: | |
break | |
for i in range(20): | |
# Calc roll time based on nav point location | |
if abs(off['roll']) > close and (180 - abs(off['roll']) > close): | |
# first roll to get the nav point at the vertical position | |
if off['yaw'] > 0 and off['pit'] > 0: | |
# top right quad, then roll right to get to 90 up | |
self.rotateRight(off['roll']) | |
elif off['yaw'] > 0 > off['pit']: | |
# bottom right quad, then roll left | |
self.rotateLeft(180 - off['roll']) | |
elif off['yaw'] < 0 < off['pit']: | |
# top left quad, then roll left | |
self.rotateLeft(-off['roll']) | |
else: | |
# bottom left quad, then roll right | |
self.rotateRight(180 + off['roll']) | |
sleep(1) | |
off = self.get_nav_offset(scr_reg) | |
else: | |
break | |
for i in range(20): | |
# Calc pitch time based on nav point location | |
if abs(off['pit']) > close: | |
if off['pit'] < 0: | |
self.pitchDown(abs(off['pit'])) | |
else: | |
self.pitchUp(abs(off['pit'])) | |
sleep(0.5) | |
off = self.get_nav_offset(scr_reg) | |
else: | |
break | |
for i in range(20): | |
# Calc yaw time based on nav point location | |
if abs(off['yaw']) > close: | |
if off['yaw'] < 0: | |
self.yawLeft(abs(off['yaw'])) | |
else: | |
self.yawRight(abs(off['yaw'])) | |
sleep(0.5) | |
off = self.get_nav_offset(scr_reg) | |
else: | |
break | |
sleep(.1) | |
logger.debug("final x:"+str(off['x'])+" y:"+str(off['y'])) | |
def fsd_target_align(self, scr_reg): | |
""" Coarse align to the target to support FSD jumping """ | |
self.vce.say("Target Align") | |
logger.debug('align= fine align') | |
close = 50 | |
# TODO: should use Pitch Rates to calculate, but this seems to work fine with all ships | |
hold_pitch = 0.150 | |
hold_yaw = 0.300 | |
for i in range(5): | |
new = self.get_destination_offset(scr_reg) | |
if new: | |
off = new | |
break | |
sleep(0.25) | |
# try one more time to align | |
if new is None: | |
self.nav_align(scr_reg) | |
new = self.get_destination_offset(scr_reg) | |
if new: | |
off = new | |
else: | |
logger.debug(' out of fine -not off-'+'\n') | |
return | |
# | |
while (off['x'] > close) or \ | |
(off['x'] < -close) or \ | |
(off['y'] > close) or \ | |
(off['y'] < -close): | |
#print("off:"+str(new)) | |
if off['x'] > close: | |
self.keys.send('YawRightButton', hold=hold_yaw) | |
if off['x'] < -close: | |
self.keys.send('YawLeftButton', hold=hold_yaw) | |
if off['y'] > close: | |
self.keys.send('PitchUpButton', hold=hold_pitch) | |
if off['y'] < -close: | |
self.keys.send('PitchDownButton', hold=hold_pitch) | |
if self.jn.ship_state()['status'] == 'starting_hyperspace': | |
return | |
for i in range(5): | |
sleep(0.1) | |
new = self.get_destination_offset(scr_reg) | |
if new: | |
off = new | |
break | |
sleep(0.25) | |
if not off: | |
return | |
logger.debug('align=complete') | |
def mnvr_to_target(self, scr_reg): | |
logger.debug('align') | |
if not (self.jn.ship_state()['status'] == 'in_supercruise' or self.jn.ship_state()['status'] == 'in_space'): | |
logger.error('align() not in sc or space') | |
raise Exception('align() not in sc or space') | |
self.sun_avoid(scr_reg) | |
self.nav_align(scr_reg) | |
self.keys.send('SetSpeed100') | |
self.fsd_target_align(scr_reg) | |
def sc_target_align(self, scr_reg) -> bool: | |
""" Stays tight on the target, monitors for disengage and obscured. | |
If target could not be found, return false.""" | |
close = 6 | |
off = None | |
hold_pitch = 0.100 | |
hold_yaw = 0.100 | |
for i in range(5): | |
new = self.get_destination_offset(scr_reg) | |
if new: | |
off = new | |
break | |
if self.is_destination_occluded(scr_reg): | |
self.occluded_reposition(scr_reg) | |
self.ap_ckb('log+vce', 'Target Align') | |
sleep(0.1) | |
# Could not be found, return | |
if off == None: | |
logger.debug("sc_target_align not finding target") | |
self.ap_ckb('log', 'Target not found, terminating SC Assist') | |
return False | |
#logger.debug("sc_target_align x: "+str(off['x'])+" y:"+str(off['y'])) | |
while (abs(off['x']) > close) or \ | |
(abs(off['y']) > close): | |
if (abs(off['x']) > 25): | |
hold_yaw = 0.2 | |
else: | |
hold_yaw = 0.09 | |
if (abs(off['y']) > 25): | |
hold_pitch = 0.15 | |
else: | |
hold_pitch = 0.075 | |
#logger.debug(" sc_target_align x: "+str(off['x'])+" y:"+str(off['y'])) | |
if off['x'] > close: | |
self.keys.send('YawRightButton', hold=hold_yaw) | |
if off['x'] < -close: | |
self.keys.send('YawLeftButton', hold=hold_yaw) | |
if off['y'] > close: | |
self.keys.send('PitchUpButton', hold=hold_pitch) | |
if off['y'] < -close: | |
self.keys.send('PitchDownButton', hold=hold_pitch) | |
sleep(.02) # time for image to catch up | |
# this checks if suddenly the target show up behind the planet | |
if self.is_destination_occluded(scr_reg): | |
self.occluded_reposition(scr_reg) | |
self.ap_ckb('log+vce', 'Target Align') | |
# check for SC Disengage | |
if self.sc_disengage_label_up(scr_reg): | |
if self.sc_disengage_active(scr_reg): | |
self.ap_ckb('log+vce', 'Disengage Supercruise') | |
self.keys.send('HyperSuperCombination') | |
self.stop_sco_monitoring() | |
break | |
new = self.get_destination_offset(scr_reg) | |
if new: | |
off = new | |
# Check if target is outside the target region (behind us) and break loop | |
if new is None: | |
logger.debug("sc_target_align lost target") | |
self.ap_ckb('log', 'Target lost, attempting re-alignment.') | |
return False | |
return True | |
def occluded_reposition(self, scr_reg): | |
""" Reposition is use when the target is occluded by a planet or other. | |
We pitch 90 deg down for a bit, then up 90, this should make the target underneath us | |
this is important because when we do nav_align() if it does not see the Nav Point | |
in the compass (because it is a hollow circle), then it will pitch down, this will | |
bring the target into view quickly. """ | |
self.ap_ckb('log+vce', 'Target occluded, repositioning.') | |
self.keys.send('SetSpeed50') | |
sleep(5) | |
self.pitchDown(90) | |
# Speed away | |
self.keys.send('SetSpeed100') | |
sleep(15) | |
self.keys.send('SetSpeed50') | |
sleep(5) | |
self.pitchUp(90) | |
self.nav_align(scr_reg) | |
self.keys.send('SetSpeed50') | |
def honk(self): | |
# Do the Discovery Scan (Honk) | |
if self.status.get_flag(FlagsAnalysisMode): | |
if self.config['DSSButton'] == 'Primary': | |
logger.debug('position=scanning') | |
self.keys.send('PrimaryFire', state=1) | |
else: | |
logger.debug('position=scanning') | |
self.keys.send('SecondaryFire', state=1) | |
sleep(7) # roughly 6 seconds for DSS | |
# stop pressing the Scanner button | |
if self.config['DSSButton'] == 'Primary': | |
logger.debug('position=scanning complete') | |
self.keys.send('PrimaryFire', state=0) | |
else: | |
logger.debug('position=scanning complete') | |
self.keys.send('SecondaryFire', state=0) | |
else: | |
self.ap_ckb('log', 'Not in analysis mode. Skipping discovery scan (honk).') | |
def logout(self): | |
""" Performs menu action to log out of game """ | |
self.update_ap_status("Logout") | |
self.keys.send_key('Down', SCANCODE["Key_Escape"]) | |
sleep(0.5) | |
self.keys.send_key('Up', SCANCODE["Key_Escape"]) | |
sleep(0.5) | |
self.keys.send('UI_Up') | |
sleep(0.5) | |
self.keys.send('UI_Select') | |
sleep(0.5) | |
self.keys.send('UI_Select') | |
sleep(0.5) | |
self.update_ap_status("Idle") | |
# position() happens after a refuel and performs | |
# - accelerate past sun | |
# - perform Discovery scan | |
# - perform fss (if enabled) | |
def position(self, scr_reg, did_refuel=True): | |
logger.debug('position') | |
add_time = 12 | |
self.vce.say("Maneuvering") | |
self.keys.send('SetSpeed100') | |
# Need time to move past Sun, account for slowed ship if refuled | |
pause_time = add_time | |
if self.config["EnableRandomness"] == True: | |
pause_time = pause_time+random.randint(0, 3) | |
# need time to get away from the Sun so heat will disipate before we use FSD | |
sleep(pause_time) | |
if self.config["ElwScannerEnable"] == True: | |
self.fss_detect_elw(scr_reg) | |
if self.config["EnableRandomness"] == True: | |
sleep(random.randint(0, 3)) | |
sleep(3) | |
else: | |
sleep(5) # since not doing FSS, need to give a little more time to get away from Sun, for heat | |
logger.debug('position=complete') | |
return True | |
# jump() happens after we are aligned to Target | |
# TODO: nees to check for Thargoid interdiction and their wave that would shut us down, | |
# if thargoid, then we wait until reboot and continue on.. go back into FSD and align | |
def jump(self, scr_reg): | |
logger.debug('jump') | |
self.vce.say("Frameshift Jump") | |
jump_tries = self.config['JumpTries'] | |
for i in range(jump_tries): | |
logger.debug('jump= try:'+str(i)) | |
if not (self.jn.ship_state()['status'] == 'in_supercruise' or self.jn.ship_state()['status'] == 'in_space'): | |
logger.error('Not ready to FSD jump. jump=err1') | |
raise Exception('not ready to jump') | |
sleep(0.5) | |
logger.debug('jump= start fsd') | |
self.keys.send('HyperSuperCombination', hold=1) | |
# Start SCO monitoring ready when we drop back to SC. | |
self.start_sco_monitoring() | |
res = self.status.wait_for_flag_on(FlagsFsdCharging, 5) | |
if not res: | |
logger.error('FSD failed to charge.') | |
continue | |
res = self.status.wait_for_flag_on(FlagsFsdJump, 30) | |
if not res: | |
logger.warning('FSD failure to start jump timeout.') | |
self.mnvr_to_target(scr_reg) # attempt realign to target | |
continue | |
logger.debug('jump= in jump') | |
# Wait for jump to complete. Should never err | |
res = self.status.wait_for_flag_off(FlagsFsdJump, 360) | |
if not res: | |
logger.error('FSD failure to complete jump timeout.') | |
continue | |
logger.debug('jump= speed 0') | |
self.jump_cnt = self.jump_cnt+1 | |
self.keys.send('SetSpeedZero', repeat=3) # Let's be triply sure that we set speed to 0% :) | |
sleep(1) # wait 1 sec after jump to allow graphics to stablize and accept inputs | |
logger.debug('jump=complete') | |
return True | |
logger.error(f'FSD Jump failed {jump_tries} times. jump=err2') | |
raise Exception("FSD Jump failure") | |
# a set of convience routes to pitch, rotate by specified degress | |
# | |
def rotateLeft(self, deg): | |
htime = deg/self.rollrate | |
self.keys.send('RollLeftButton', hold=htime) | |
def rotateRight(self, deg): | |
htime = deg/self.rollrate | |
self.keys.send('RollRightButton', hold=htime) | |
def pitchDown(self, deg): | |
htime = deg/self.pitchrate | |
self.keys.send('PitchDownButton', htime) | |
def pitchUp(self, deg): | |
htime = deg/self.pitchrate | |
self.keys.send('PitchUpButton', htime) | |
def yawLeft(self, deg): | |
htime = deg/self.yawrate | |
self.keys.send('YawLeftButton', hold=htime) | |
def yawRight(self, deg): | |
htime = deg / self.yawrate | |
self.keys.send('YawRightButton', hold=htime) | |
def refuel(self, scr_reg): | |
""" Check if refueling needed, ensure correct start type. """ | |
# Check if we have a fuel scoop | |
has_fuel_scoop = self.jn.ship_state()['has_fuel_scoop'] | |
logger.debug('refuel') | |
scoopable_stars = ['F', 'O', 'G', 'K', 'B', 'A', 'M'] | |
if self.jn.ship_state()['status'] != 'in_supercruise': | |
logger.error('refuel=err1') | |
return False | |
raise Exception('not ready to refuel') | |
is_star_scoopable = self.jn.ship_state()['star_class'] in scoopable_stars | |
# if the sun is not scoopable, then set a low low threshold so we can pick up the dull red | |
# sun types. Since we won't scoop it doesn't matter how much we pitch up | |
# if scoopable we know white/yellow stars are bright, so set higher threshold, this will allow us to | |
# mast out the galaxy edge (which is bright) and not pitch up too much and avoid scooping | |
if is_star_scoopable == False or not has_fuel_scoop: | |
scr_reg.set_sun_threshold(25) | |
else: | |
scr_reg.set_sun_threshold(self.config['SunBrightThreshold']) | |
# Lets avoid the sun, shall we | |
self.vce.say("Avoiding star") | |
self.update_ap_status("Avoiding star") | |
self.ap_ckb('log', 'Avoiding star') | |
self.sun_avoid(scr_reg) | |
if self.jn.ship_state()['fuel_percent'] < self.config['RefuelThreshold'] and is_star_scoopable and has_fuel_scoop: | |
logger.debug('refuel= start refuel') | |
self.vce.say("Refueling") | |
self.ap_ckb('log', 'Refueling') | |
self.update_ap_status("Refueling") | |
# mnvr into position | |
self.keys.send('SetSpeed100') | |
sleep(5) | |
self.keys.send('SetSpeed50') | |
sleep(1.7) | |
self.keys.send('SetSpeedZero', repeat=3) | |
self.refuel_cnt += 1 | |
# The log will not reflect a FuelScoop until first 5 tons filled, then every 5 tons until complete | |
#if we don't scoop first 5 tons with 40 sec break, since not scooping or not fast enough or not at all, then abort | |
startime = time.time() | |
while not self.jn.ship_state()['is_scooping'] and not self.jn.ship_state()['fuel_percent'] == 100: | |
# check if we are being interdicted | |
interdicted = self.interdiction_check() | |
if interdicted: | |
# Continue journey after interdiction | |
self.keys.send('SetSpeedZero') | |
if ((time.time()-startime) > int(self.config['FuelScoopTimeOut'])): | |
self.vce.say("Refueling abort, insufficient scooping") | |
return False | |
logger.debug('refuel= wait for refuel') | |
# We started fueling, so lets give it another timeout period to fuel up | |
startime = time.time() | |
while not self.jn.ship_state()['fuel_percent'] == 100: | |
# check if we are being interdicted | |
interdicted = self.interdiction_check() | |
if interdicted: | |
# Continue journey after interdiction | |
self.keys.send('SetSpeedZero') | |
if ((time.time()-startime) > int(self.config['FuelScoopTimeOut'])): | |
self.vce.say("Refueling abort, insufficient scooping") | |
return True | |
sleep(1) | |
logger.debug('refuel=complete') | |
return True | |
elif is_star_scoopable == False: | |
self.ap_ckb('log', 'Skip refuel - not a fuel star') | |
logger.debug('refuel= needed, unsuitable star') | |
self.pitchUp(20) | |
return False | |
elif self.jn.ship_state()['fuel_percent'] >= self.config['RefuelThreshold']: | |
self.ap_ckb('log', 'Skip refuel - fuel level okay') | |
logger.debug('refuel= not needed') | |
return False | |
elif not has_fuel_scoop: | |
self.ap_ckb('log', 'Skip refuel - no fuel scoop fitted') | |
logger.debug('No fuel scoop fitted.') | |
self.pitchUp(20) | |
return False | |
else: | |
self.pitchUp(15) # if not refueling pitch up somemore so we won't heat up | |
return False | |
# set focus to the ED window, if ED does not have focus then the key strokes will go to the window | |
# that does have focus | |
def set_focus_elite_window(self): | |
handle = win32gui.FindWindow(0, "Elite - Dangerous (CLIENT)") | |
if handle != 0: | |
win32gui.SetForegroundWindow(handle) # give focus to ED | |
def waypoint_undock_seq(self): | |
self.update_ap_status("Executing Undocking/Launch") | |
# Store current location (on planet or in space) | |
on_planet = self.status.get_flag(FlagsHasLatLong) | |
on_orbital_construction_site = self.jn.ship_state()['cur_station_type'].upper() == "SpaceConstructionDepot".upper() | |
fleet_carrier = self.jn.ship_state()['cur_station_type'].upper() == "FleetCarrier".upper() | |
# Leave starport or planetary port | |
if not on_planet: | |
# Check that we are docked | |
if self.status.get_flag(FlagsDocked): | |
# Check if we have an advanced docking computer | |
if not self.jn.ship_state()['has_adv_dock_comp']: | |
self.ap_ckb('log', "Unable to undock. Advanced Docking Computer not fitted.") | |
logger.warning('Unable to undock. Advanced Docking Computer not fitted.') | |
raise Exception('Unable to undock. Advanced Docking Computer not fitted.') | |
# Undock from station | |
self.undock() | |
# need to wait until undock complete, that is when we are back in_space | |
while self.jn.ship_state()['status'] != 'in_space': | |
sleep(1) | |
# If we are on an Orbital Construction Site we will need to pitch up 90 deg to avoid crashes | |
if on_orbital_construction_site: | |
self.ap_ckb('log+vce', 'Maneuvering') | |
# The pitch rates are defined in SC, not normal flights, so bump this up a bit | |
self.pitchUp(90 * 1.25) | |
# If we are on a Fleet Carrier we will pitch up 90 deg and fly away to avoid planet | |
elif fleet_carrier: | |
self.ap_ckb('log+vce', 'Maneuvering') | |
# The pitch rates are defined in SC, not normal flights, so bump this up a bit | |
self.pitchUp(90 * 1.25) | |
self.keys.send('SetSpeed100') | |
# While Mass Locked, keep boosting. | |
while not self.status.wait_for_flag_off(FlagsFsdMassLocked, timeout=2): | |
self.keys.send('UseBoostJuice') | |
# Enter supercruise to get away from the Fleet Carrier | |
self.keys.send('Supercruise') | |
# Start SCO monitoring | |
self.start_sco_monitoring() | |
# Wait the configured time before continuing | |
self.ap_ckb('log', 'Flying for configured FC departure time.') | |
sleep(self.config['FCDepartureTime']) | |
self.keys.send('SetSpeed50') | |
if not fleet_carrier: | |
# In space (launched from starport or outpost etc.) | |
sleep(1.5) | |
self.update_ap_status("Undock Complete, accelerating") | |
self.keys.send('SetSpeed100') | |
sleep(1) | |
self.keys.send('UseBoostJuice') | |
sleep(13) # get away from Station | |
self.keys.send('SetSpeed50') | |
elif on_planet: | |
# Check if we are on a landing pad (docked), or landed on the planet surface | |
if self.status.get_flag(FlagsDocked): | |
# We are on a landing pad (docked) | |
# Check if we have an advanced docking computer | |
if not self.jn.ship_state()['has_adv_dock_comp']: | |
self.ap_ckb('log', "Unable to undock. Advanced Docking Computer not fitted.") | |
logger.warning('Unable to undock. Advanced Docking Computer not fitted.') | |
raise Exception('Unable to undock. Advanced Docking Computer not fitted.') | |
# Undock from port | |
self.undock() | |
# need to wait until undock complete, that is when we are back in_space | |
while self.jn.ship_state()['status'] != 'in_space': | |
sleep(1) | |
self.update_ap_status("Undock Complete, accelerating") | |
elif self.status.get_flag(FlagsLanded): | |
# We are on planet surface (not docked at planet landing pad) | |
# Hold UP for takeoff | |
self.keys.send('UpThrustButton', hold=6) | |
self.keys.send('LandingGearToggle') | |
self.update_ap_status("Takeoff Complete, accelerating") | |
# Undocked or off the surface, so leave planet | |
self.keys.send('SetSpeed50') | |
# The pitch rates are defined in SC, not normal flights, so bump this up a bit | |
self.pitchUp(90 * 1.25) | |
self.keys.send('SetSpeed100') | |
# While Mass Locked, keep boosting. | |
while not self.status.wait_for_flag_off(FlagsFsdMassLocked, timeout=2): | |
self.keys.send('UseBoostJuice') | |
# Enter supercruise | |
self.keys.send('Supercruise') | |
# Start SCO monitoring | |
self.start_sco_monitoring() | |
# Wait for SC | |
res = self.status.wait_for_flag_on(FlagsSupercruise, timeout=30) | |
# Enable SCO. If SCO not fitted, this will do nothing. | |
self.keys.send('UseBoostJuice') | |
# Wait until out of orbit. | |
res = self.status.wait_for_flag_off(FlagsHasLatLong, timeout=60) | |
# TODO - do we need to check if we never leave orbit? | |
# Disable SCO. If SCO not fitted, this will do nothing. | |
self.keys.send('UseBoostJuice') | |
self.keys.send('SetSpeed50') | |
def sc_engage(self): | |
""" Engages supercruise, then returns us to 50% speed """ | |
self.keys.send('SetSpeed100') | |
self.keys.send('Supercruise', hold=0.001) | |
# Start SCO monitoring | |
self.start_sco_monitoring() | |
# TODO - check if we actually go into supercruise | |
sleep(12) | |
self.keys.send('SetSpeed50') | |
def waypoint_assist(self, keys, scr_reg): | |
""" Processes the waypoints, performing jumps and sc assist if going to a station | |
also can then perform trades if specific in the waypoints file.""" | |
self.waypoint.waypoint_assist(keys, scr_reg) | |
def jump_to_system(self, scr_reg, system_name: str) -> bool: | |
""" Jumps to the specified system. Returns True if in the system already, | |
or we successfully travel there, else False. """ | |
self.update_ap_status(f"Targeting System: {system_name}") | |
ret = self.galaxy_map.set_next_system(self, system_name) | |
if not ret: | |
return False | |
# if we are starting the waypoint docked at a station, we need to undock first | |
if self.status.get_flag(FlagsDocked) or self.status.get_flag(FlagsLanded): | |
self.waypoint_undock_seq() | |
# if we are in space but not in supercruise, get into supercruise | |
if self.jn.ship_state()['status'] != 'in_supercruise': | |
self.sc_engage() | |
# Route sent... FSD Assist to that destination | |
reached_dest = self.fsd_assist(scr_reg) | |
if not reached_dest: | |
return False | |
return True | |
def supercruise_to_station(self, scr_reg, station_name: str) -> bool: | |
""" Supercruise to the specified target, which may be a station, FC, body, signal source, etc. | |
Returns True if we travel successfully travel there, else False. """ | |
# If waypoint file has a Station Name associated then attempt targeting it | |
self.update_ap_status(f"Targeting Station: {station_name}") | |
#res = self.nav_panel.lock_destination(station_name) | |
#if not res: | |
# return False | |
# if we are starting the waypoint docked at a station, we need to undock first | |
if self.status.get_flag(FlagsDocked) or self.status.get_flag(FlagsLanded): | |
self.waypoint_undock_seq() | |
# if we are in space but not in supercruise, get into supercruise | |
if self.jn.ship_state()['status'] != 'in_supercruise': | |
self.sc_engage() | |
# Successful targeting of Station, lets go to it | |
sleep(3) # Wait for compass to stop flashing blue! | |
if self.have_destination(scr_reg): | |
self.ap_ckb('log', " - Station: " + station_name) | |
self.update_ap_status(f"SC to Station: {station_name}") | |
self.sc_assist(scr_reg) | |
else: | |
self.ap_ckb('log', f" - Could not target station: {station_name}") | |
return False | |
return True | |
def fsd_assist(self, scr_reg): | |
""" FSD Route Assist. """ | |
logger.debug('self.jn.ship_state='+str(self.jn.ship_state())) | |
starttime = time.time() | |
starttime -= 20 # to account for first instance not doing positioning | |
if self.jn.ship_state()['target']: | |
# if we are starting the waypoint docked at a station, we need to undock first | |
if self.status.get_flag(FlagsDocked) or self.status.get_flag(FlagsLanded): | |
self.update_overlay() | |
self.waypoint_undock_seq() | |
while self.jn.ship_state()['target']: | |
self.update_overlay() | |
if self.jn.ship_state()['status'] == 'in_space' or self.jn.ship_state()['status'] == 'in_supercruise': | |
self.update_ap_status("Align") | |
self.mnvr_to_target(scr_reg) | |
self.update_ap_status("Jump") | |
self.jump(scr_reg) | |
# update jump counters | |
self.total_dist_jumped += self.jn.ship_state()['dist_jumped'] | |
self.total_jumps = self.jump_cnt+self.jn.ship_state()['jumps_remains'] | |
# reset, upon next Jump the Journal will be updated again, unless last jump, | |
# so we need to clear this out | |
self.jn.ship_state()['jumps_remains'] = 0 | |
self.update_overlay() | |
avg_time_jump = (time.time()-starttime)/self.jump_cnt | |
self.ap_ckb('jumpcount', "Dist: {:,.1f}".format(self.total_dist_jumped)+"ly"+ | |
" Jumps: {}of{}".format(self.jump_cnt, self.total_jumps)+" @{}s/j".format(int(avg_time_jump))+ | |
" Fu#: "+str(self.refuel_cnt)) | |
# Do the Discovery Scan (Honk) | |
self.honk_thread = threading.Thread(target=self.honk, daemon=True) | |
self.honk_thread.start() | |
# Refuel | |
refueled = self.refuel(scr_reg) | |
self.update_ap_status("Maneuvering") | |
self.position(scr_reg, refueled) | |
if self.jn.ship_state()['fuel_percent'] < self.config['FuelThreasholdAbortAP']: | |
self.ap_ckb('log', "AP Aborting, low fuel") | |
self.vce.say("AP Aborting, low fuel") | |
break | |
sleep(2) # wait until screen stabilizes from possible last positioning | |
# if there is no destination defined, we are done | |
if not self.have_destination(scr_reg): | |
self.keys.send('SetSpeedZero') | |
self.vce.say("Destination Reached, distance jumped:"+str(int(self.total_dist_jumped))+" lightyears") | |
if self.config["AutomaticLogout"] == True: | |
sleep(5) | |
self.logout() | |
return True | |
# else there is a destination in System, so let jump over to SC Assist | |
else: | |
self.keys.send('SetSpeed100') | |
self.vce.say("System Reached, preparing for supercruise") | |
sleep(1) | |
return False | |
# Supercruise Assist loop to travel to target in system and perform autodock | |
# | |
def sc_assist(self, scr_reg, do_docking=True): | |
logger.debug("Entered sc_assist") | |
# Goto cockpit view | |
self.ship_control.goto_cockpit_view() | |
align_failed = False | |
# see if we have a compass up, if so then we have a target | |
if not self.have_destination(scr_reg): | |
self.ap_ckb('log', "Quiting SC Assist - Compass not found. Rotate ship and try again.") | |
logger.debug("Quiting sc_assist - compass not found") | |
return | |
# if we are starting the waypoint docked at a station or landed, we need to undock/takeoff first | |
if self.status.get_flag(FlagsDocked) or self.status.get_flag(FlagsLanded): | |
self.update_overlay() | |
self.waypoint_undock_seq() | |
# if we are in space but not in supercruise, get into supercruise | |
if self.jn.ship_state()['status'] != 'in_supercruise': | |
self.sc_engage() | |
# Ensure we are 50%, don't want the loop of shame | |
# Align Nav to target | |
self.keys.send('SetSpeed50') | |
self.nav_align(scr_reg) # Compass Align | |
self.keys.send('SetSpeed50') | |
self.jn.ship_state()['interdicted'] = False | |
# Loop forever keeping tight align to target, until we get SC Disengage popup | |
self.ap_ckb('log+vce', 'Target Align') | |
while True: | |
sleep(0.05) | |
if self.jn.ship_state()['status'] == 'in_supercruise': | |
# Align and stay on target. If false is returned, we have lost the target behind us. | |
if not self.sc_target_align(scr_reg): | |
# Continue ahead before aligning to prevent us circling the target | |
# self.keys.send('SetSpeed100') | |
sleep(10) | |
self.keys.send('SetSpeed50') | |
self.nav_align(scr_reg) # Compass Align | |
elif self.status.get_flag2(Flags2GlideMode): | |
# Gliding - wait to complete | |
logger.debug("Gliding") | |
self.status.wait_for_flag2_off(Flags2GlideMode, 30) | |
break | |
else: | |
# if we dropped from SC, then we rammed into planet | |
logger.debug("No longer in supercruise") | |
align_failed = True | |
break | |
# check if we are being interdicted | |
interdicted = self.interdiction_check() | |
if interdicted: | |
# Continue journey after interdiction | |
self.keys.send('SetSpeed50') | |
self.nav_align(scr_reg) # realign with station | |
# check for SC Disengage | |
if self.sc_disengage_label_up(scr_reg): | |
if self.sc_disengage_active(scr_reg): | |
self.ap_ckb('log+vce', 'Disengage Supercruise') | |
self.keys.send('HyperSuperCombination') | |
self.stop_sco_monitoring() | |
break | |
# if no error, we must have gotten disengage | |
if not align_failed and do_docking: | |
sleep(4) # wait for the journal to catch up | |
# Check if this is a target we cannot dock at | |
skip_docking = False | |
if not self.jn.ship_state()['has_adv_dock_comp'] and not self.jn.ship_state()['has_std_dock_comp']: | |
self.ap_ckb('log', "Skipping docking. No Docking Computer fitted.") | |
skip_docking = True | |
if not self.jn.ship_state()['SupercruiseDestinationDrop_type'] is None: | |
if (self.jn.ship_state()['SupercruiseDestinationDrop_type'].startswith("$USS_Type") | |
# Bulk Cruisers | |
or "-class Cropper" in self.jn.ship_state()['SupercruiseDestinationDrop_type'] | |
or "-class Hauler" in self.jn.ship_state()['SupercruiseDestinationDrop_type'] | |
or "-class Reformatory" in self.jn.ship_state()['SupercruiseDestinationDrop_type'] | |
or "-class Researcher" in self.jn.ship_state()['SupercruiseDestinationDrop_type'] | |
or "-class Surveyor" in self.jn.ship_state()['SupercruiseDestinationDrop_type'] | |
or "-class Traveller" in self.jn.ship_state()['SupercruiseDestinationDrop_type'] | |
or "-class Tanker" in self.jn.ship_state()['SupercruiseDestinationDrop_type']): | |
self.ap_ckb('log', "Skipping docking. No docking privilege at MegaShips.") | |
skip_docking = True | |
if not skip_docking: | |
# go into docking sequence | |
self.dock() | |
self.ap_ckb('log+vce', "Docking complete, refueled, repaired and re-armed") | |
self.update_ap_status("Docking Complete") | |
else: | |
self.keys.send('SetSpeedZero') | |
else: | |
self.vce.say("Exiting Supercruise, setting throttle to zero") | |
self.keys.send('SetSpeedZero') # make sure we don't continue to land | |
self.ap_ckb('log', "Supercruise dropped, terminating SC Assist") | |
self.ap_ckb('log+vce', "Supercruise Assist complete") | |
def robigo_assist(self): | |
self.robigo.loop(self) | |
# Simply monitor for Shields down so we can boost away or our fighter got destroyed | |
# and thus redeploy another one | |
def afk_combat_loop(self): | |
while True: | |
if self.afk_combat.check_shields_up() == False: | |
self.set_focus_elite_window() | |
self.vce.say("Shields down, evading") | |
self.afk_combat.evade() | |
# after supercruise the menu is reset to top | |
self.afk_combat.launch_fighter() # at new location launch fighter | |
break | |
if self.afk_combat.check_fighter_destroyed() == True: | |
self.set_focus_elite_window() | |
self.vce.say("Fighter Destroyed, redeploying") | |
self.afk_combat.launch_fighter() # assuming two fighter bays | |
self.vce.say("Terminating AFK Combat Assist") | |
def dss_assist(self): | |
while True: | |
sleep(0.5) | |
if self.jn.ship_state()['status'] == 'in_supercruise': | |
cur_star_system = self.jn.ship_state()['cur_star_system'] | |
if cur_star_system != self._prev_star_system: | |
self.update_ap_status("DSS Scan") | |
self.ap_ckb('log', 'DSS Scan: '+cur_star_system) | |
self.set_focus_elite_window() | |
self.honk() | |
self._prev_star_system = cur_star_system | |
self.update_ap_status("Idle") | |
def single_waypoint_assist(self): | |
""" Travel to a system or station or both.""" | |
if self._single_waypoint_system == "" and self._single_waypoint_station == "": | |
return False | |
if self._single_waypoint_system != "": | |
res = self.jump_to_system(self.scrReg, self._single_waypoint_system) | |
if res is False: | |
return False | |
# Disabled until SC to station enabled. | |
# if self._single_waypoint_station != "": | |
# res = self.supercruise_to_station(self.scrReg, self._single_waypoint_station) | |
# if res is False: | |
# return False | |
# raising an exception to the engine loop thread, so we can terminate its execution | |
# if thread was in a sleep, the exception seems to not be delivered | |
def ctype_async_raise(self, thread_obj, exception): | |
found = False | |
target_tid = 0 | |
for tid, tobj in threading._active.items(): | |
if tobj is thread_obj: | |
found = True | |
target_tid = tid | |
break | |
if not found: | |
raise ValueError("Invalid thread object") | |
ret = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(target_tid), | |
ctypes.py_object(exception)) | |
# ref: http://docs.python.org/c-api/init.html#PyThreadState_SetAsyncExc | |
if ret == 0: | |
raise ValueError("Invalid thread ID") | |
elif ret > 1: | |
# Huh? Why would we notify more than one threads? | |
# Because we punch a hole into C level interpreter. | |
# So it is better to clean up the mess. | |
ctypes.pythonapi.PyThreadState_SetAsyncExc(target_tid, 0) | |
raise SystemError("PyThreadState_SetAsyncExc failed") | |
# | |
# Setter routines for state variables | |
# | |
def set_fsd_assist(self, enable=True): | |
if enable == False and self.fsd_assist_enabled == True: | |
self.ctype_async_raise(self.ap_thread, EDAP_Interrupt) | |
self.fsd_assist_enabled = enable | |
def set_sc_assist(self, enable=True): | |
if enable == False and self.sc_assist_enabled == True: | |
self.ctype_async_raise(self.ap_thread, EDAP_Interrupt) | |
self.sc_assist_enabled = enable | |
def set_waypoint_assist(self, enable=True): | |
if enable == False and self.waypoint_assist_enabled == True: | |
self.ctype_async_raise(self.ap_thread, EDAP_Interrupt) | |
self.waypoint_assist_enabled = enable | |
def set_robigo_assist(self, enable=True): | |
if enable == False and self.robigo_assist_enabled == True: | |
self.ctype_async_raise(self.ap_thread, EDAP_Interrupt) | |
self.robigo_assist_enabled = enable | |
def set_afk_combat_assist(self, enable=True): | |
if enable == False and self.afk_combat_assist_enabled == True: | |
self.ctype_async_raise(self.ap_thread, EDAP_Interrupt) | |
self.afk_combat_assist_enabled = enable | |
def set_dss_assist(self, enable=True): | |
if enable == False and self.dss_assist_enabled == True: | |
self.ctype_async_raise(self.ap_thread, EDAP_Interrupt) | |
self.dss_assist_enabled = enable | |
def set_single_waypoint_assist(self, system: str, station: str, enable=True): | |
if enable == False and self.single_waypoint_enabled == True: | |
self.ctype_async_raise(self.ap_thread, EDAP_Interrupt) | |
self._single_waypoint_system = system | |
self._single_waypoint_station = station | |
self.single_waypoint_enabled = enable | |
def set_cv_view(self, enable=True, x=0, y=0): | |
self.cv_view = enable | |
self.config['Enable_CV_View'] = int(self.cv_view) # update the config | |
self.update_config() # save the config | |
if enable == True: | |
self.cv_view_x = x | |
self.cv_view_y = y | |
else: | |
cv2.destroyAllWindows() | |
cv2.waitKey(50) | |
def set_randomness(self, enable=False): | |
self.config["EnableRandomness"] = enable | |
def set_activate_elite_eachkey(self, enable=False): | |
self.config["ActivateEliteEachKey"] = enable | |
def set_automatic_logout(self, enable=False): | |
self.config["AutomaticLogout"] = enable | |
def set_overlay(self, enable=False): | |
# TODO: apply the change without restarting the program | |
self.config["OverlayTextEnable"] = enable | |
if not enable: | |
self.overlay.overlay_clear() | |
self.overlay.overlay_paint() | |
def set_voice(self, enable=False): | |
if enable == True: | |
self.vce.set_on() | |
else: | |
self.vce.set_off() | |
def set_fss_scan(self, enable=False): | |
self.config["ElwScannerEnable"] = enable | |
def set_log_error(self, enable=False): | |
self.config["LogDEBUG"] = False | |
self.config["LogINFO"] = False | |
logger.setLevel(logging.ERROR) | |
def set_log_debug(self, enable=False): | |
self.config["LogDEBUG"] = True | |
self.config["LogINFO"] = False | |
logger.setLevel(logging.DEBUG) | |
def set_log_info(self, enable=False): | |
self.config["LogDEBUG"] = False | |
self.config["LogINFO"] = True | |
logger.setLevel(logging.INFO) | |
# quit() is important to call to clean up, if we don't terminate the threads we created the AP will hang on exit | |
# have then then kill python exec | |
def quit(self): | |
if self.vce != None: | |
self.vce.quit() | |
if self.overlay != None: | |
self.overlay.overlay_quit() | |
self.terminate = True | |
# | |
# This function will execute in its own thread and will loop forever until | |
# the self.terminate flag is set | |
# | |
def engine_loop(self): | |
while not self.terminate: | |
self._sc_sco_active_loop_enable = True | |
if self._sc_sco_active_loop_enable: | |
if self._sc_sco_active_loop_thread is None or not self._sc_sco_active_loop_thread.is_alive(): | |
self._sc_sco_active_loop_thread = threading.Thread(target=self._sc_sco_active_loop, daemon=True) | |
self._sc_sco_active_loop_thread.start() | |
if self.fsd_assist_enabled == True: | |
logger.debug("Running fsd_assist") | |
self.set_focus_elite_window() | |
self.update_overlay() | |
self.jump_cnt = 0 | |
self.refuel_cnt = 0 | |
self.total_dist_jumped = 0 | |
self.total_jumps = 0 | |
fin = True | |
# could be deep in call tree when user disables FSD, so need to trap that exception | |
try: | |
fin = self.fsd_assist(self.scrReg) | |
except EDAP_Interrupt: | |
logger.debug("Caught stop exception") | |
except Exception as e: | |
print("Trapped generic:"+str(e)) | |
traceback.print_exc() | |
self.fsd_assist_enabled = False | |
self.ap_ckb('fsd_stop') | |
self.update_overlay() | |
# if fsd_assist returned false then we are not finished, meaning we have an in system target | |
# defined. So lets enable Supercruise assist to get us there | |
# Note: this is tricky, in normal FSD jumps the target is pretty much on the other side of Sun | |
# when we arrive, but not so when we are in the final system | |
if fin == False: | |
self.ap_ckb("sc_start") | |
# drop all out debug windows | |
#cv2.destroyAllWindows() | |
#cv2.waitKey(10) | |
elif self.sc_assist_enabled == True: | |
logger.debug("Running sc_assist") | |
self.set_focus_elite_window() | |
self.update_overlay() | |
try: | |
self.update_ap_status("SC to Target") | |
self.sc_assist(self.scrReg) | |
except EDAP_Interrupt: | |
logger.debug("Caught stop exception") | |
except Exception as e: | |
print("Trapped generic:"+str(e)) | |
traceback.print_exc() | |
logger.debug("Completed sc_assist") | |
self.sc_assist_enabled = False | |
self.ap_ckb('sc_stop') | |
self.update_overlay() | |
elif self.waypoint_assist_enabled == True: | |
logger.debug("Running waypoint_assist") | |
self.set_focus_elite_window() | |
self.update_overlay() | |
self.jump_cnt = 0 | |
self.refuel_cnt = 0 | |
self.total_dist_jumped = 0 | |
self.total_jumps = 0 | |
try: | |
self.waypoint_assist(self.keys, self.scrReg) | |
except EDAP_Interrupt: | |
logger.debug("Caught stop exception") | |
except Exception as e: | |
print("Trapped generic:"+str(e)) | |
traceback.print_exc() | |
self.waypoint_assist_enabled = False | |
self.ap_ckb('waypoint_stop') | |
self.update_overlay() | |
elif self.robigo_assist_enabled == True: | |
logger.debug("Running robigo_assist") | |
self.set_focus_elite_window() | |
self.update_overlay() | |
try: | |
self.robigo_assist() | |
except EDAP_Interrupt: | |
logger.debug("Caught stop exception") | |
except Exception as e: | |
print("Trapped generic:"+str(e)) | |
traceback.print_exc() | |
self.robigo_assist_enabled = False | |
self.ap_ckb('robigo_stop') | |
self.update_overlay() | |
elif self.afk_combat_assist_enabled == True: | |
self.update_overlay() | |
try: | |
self.afk_combat_loop() | |
except EDAP_Interrupt: | |
logger.debug("Stopping afk_combat") | |
self.afk_combat_assist_enabled = False | |
self.ap_ckb('afk_stop') | |
self.update_overlay() | |
elif self.dss_assist_enabled == True: | |
logger.debug("Running dss_assist") | |
self.set_focus_elite_window() | |
self.update_overlay() | |
try: | |
self.dss_assist() | |
except EDAP_Interrupt: | |
logger.debug("Stopping DSS Assist") | |
self.dss_assist_enabled = False | |
self.ap_ckb('dss_stop') | |
self.update_overlay() | |
elif self.single_waypoint_enabled: | |
self.update_overlay() | |
try: | |
self.single_waypoint_assist() | |
except EDAP_Interrupt: | |
logger.debug("Stopping Single Waypoint Assist") | |
self.single_waypoint_enabled = False | |
self.ap_ckb('single_waypoint_stop') | |
self.update_overlay() | |
# Check once EDAPGUI loaded to prevent errors logging to the listbox before loaded | |
if self.gui_loaded: | |
# Check if ship has changed | |
ship = self.jn.ship_state()['type'] | |
# Check if a ship and not a suit (on foot) | |
if ship not in ship_size_map: | |
# Clear current ship | |
self.current_ship_type = '' | |
else: | |
ship_fullname = get_ship_fullname(ship) | |
# Check if ship changed or just loaded | |
if ship != self.current_ship_type: | |
if self.current_ship_type is not None: | |
cur_ship_fullname = get_ship_fullname(self.current_ship_type) | |
self.ap_ckb('log+vce', f"Switched ship from your {cur_ship_fullname} to your {ship_fullname}.") | |
else: | |
self.ap_ckb('log+vce', f"Welcome aboard your {ship_fullname}.") | |
# Check for fuel scoop and advanced docking computer | |
if not self.jn.ship_state()['has_fuel_scoop']: | |
self.ap_ckb('log+vce', f"Warning, your {ship_fullname} is not fitted with a Fuel Scoop.") | |
if not self.jn.ship_state()['has_adv_dock_comp']: | |
self.ap_ckb('log+vce', f"Warning, your {ship_fullname} is not fitted with an Advanced Docking Computer.") | |
if self.jn.ship_state()['has_std_dock_comp']: | |
self.ap_ckb('log+vce', f"Warning, your {ship_fullname} is fitted with a Standard Docking Computer.") | |
# Add ship to ship configs if missing | |
if ship is not None: | |
if ship not in self.ship_configs['Ship_Configs']: | |
self.ship_configs['Ship_Configs'][ship] = dict() | |
current_ship_cfg = self.ship_configs['Ship_Configs'][ship] | |
self.compass_scale = current_ship_cfg.get('compass_scale', self.scr.scaleX) | |
self.rollrate = current_ship_cfg.get('RollRate', 80.0) | |
self.pitchrate = current_ship_cfg.get('PitchRate', 33.0) | |
self.yawrate = current_ship_cfg.get('YawRate', 8.0) | |
self.sunpitchuptime = current_ship_cfg.get('SunPitchUp+Time', 0.0) | |
# Update GUI | |
self.ap_ckb('update_ship_cfg') | |
# Store ship for change detection | |
self.current_ship_type = ship | |
# Reload templates | |
self.templ.reload_templates(self.scr.scaleX, self.scr.scaleY, self.compass_scale) | |
self.update_overlay() | |
cv2.waitKey(10) | |
sleep(1) | |
def ship_tst_pitch(self): | |
""" Performs a ship pitch test by pitching 360 degrees. | |
If the ship does not rotate enough, decrease the pitch value. | |
If the ship rotates too much, increase the pitch value. | |
""" | |
if not self.status.get_flag(FlagsSupercruise): | |
self.ap_ckb('log', "Enter Supercruise and try again.") | |
return | |
if self.jn.ship_state()['target'] is None: | |
self.ap_ckb('log', "Select a target system and try again.") | |
return | |
self.set_focus_elite_window() | |
sleep(0.25) | |
self.keys.send('SetSpeed50') | |
self.pitchUp(360) | |
def ship_tst_roll(self): | |
""" Performs a ship roll test by pitching 360 degrees. | |
If the ship does not rotate enough, decrease the roll value. | |
If the ship rotates too much, increase the roll value. | |
""" | |
if not self.status.get_flag(FlagsSupercruise): | |
self.ap_ckb('log', "Enter Supercruise and try again.") | |
return | |
if self.jn.ship_state()['target'] is None: | |
self.ap_ckb('log', "Select a target system and try again.") | |
return | |
self.set_focus_elite_window() | |
sleep(0.25) | |
self.keys.send('SetSpeed50') | |
self.rotateLeft(360) | |
def ship_tst_yaw(self): | |
""" Performs a ship yaw test by pitching 360 degrees. | |
If the ship does not rotate enough, decrease the yaw value. | |
If the ship rotates too much, increase the yaw value. | |
""" | |
if not self.status.get_flag(FlagsSupercruise): | |
self.ap_ckb('log', "Enter Supercruise and try again.") | |
return | |
if self.jn.ship_state()['target'] is None: | |
self.ap_ckb('log', "Select a target system and try again.") | |
return | |
self.set_focus_elite_window() | |
sleep(0.25) | |
self.keys.send('SetSpeed50') | |
self.yawLeft(360) | |
# | |
# This main is for testing purposes. | |
# | |
def main(): | |
#handle = win32gui.FindWindow(0, "Elite - Dangerous (CLIENT)") | |
#if handle != None: | |
# win32gui.SetForegroundWindow(handle) # put the window in foreground | |
ed_ap = EDAutopilot(cb=None, doThread=False) | |
ed_ap.cv_view = True | |
ed_ap.cv_view_x = 4000 | |
ed_ap.cv_view_y = 100 | |
sleep(2) | |
for x in range(10): | |
#target_align(scrReg) | |
print("Calling nav_align") | |
#ed_ap.nav_align(ed_ap.scrReg) | |
ed_ap.fss_detect_elw(ed_ap.scrReg) | |
#loc = get_destination_offset(scrReg) | |
#print("get_dest: " +str(loc)) | |
#loc = get_nav_offset(scrReg) | |
#print("get_nav: " +str(loc)) | |
cv2.waitKey(0) | |
print("Done nav") | |
sleep(8) | |
ed_ap.overlay.overlay_quit() | |
if __name__ == "__main__": | |
main() | |
--- | |
File: /EDafk_combat.py | |
--- | |
from time import sleep | |
import keyboard | |
import win32gui | |
from EDJournal import * | |
from EDKeys import * | |
from EDlogger import logger | |
from Voice import * | |
""" | |
Description: AFK Combat in Rez site (see Type 10 AFK videos). | |
This script will monitor shields (from Journal file), if shields are lost (ShieldsUp == False) then | |
boost and supercruise away, after a bit, drop from supercruise, pips to System/Weapons and deploy fighter | |
This script will also monitor if a deployed Fighter is destroyed, if so it will deploy another fighter, | |
this requires another fighter bay with a fighter ready (could modify to wait for fighter rebuild) | |
Required State: | |
- Point away from Rez sight (so when boost away can get out of mass lock) | |
Author: [email protected] | |
""" | |
""" | |
TODO: consider the following enhancements | |
- If a key is not mapped need to list which are needed and exit | |
- Need key for commading fighter to 'attack at will' or defend | |
- After retreat complete, could go into while loop looking for "UnderAttack" or | |
simply go back into shields down logic again. Would have to wait until shields are up | |
- | |
""" | |
class AFK_Combat: | |
def __init__(self, k, journal, v=None): | |
self.k = k | |
self.jn = journal | |
self.voice = v | |
self.fighter_bay = 2 | |
def check_shields_up(self): | |
return self.jn.ship_state()['shieldsup'] | |
def check_fighter_destroyed(self): | |
des = self.jn.ship_state()['fighter_destroyed'] | |
self.jn.ship_state()['fighter_destroyed'] = False # reset it to false after we read it | |
return des | |
def set_focus_elite_window(self): | |
handle = win32gui.FindWindow(0, "Elite - Dangerous (CLIENT)") | |
if handle != None: | |
win32gui.SetForegroundWindow(handle) # give focus to ED | |
def evade(self): | |
# boost and prep for supercruise | |
if self.voice != None: | |
self.voice.say("Boosting, reconfiguring") | |
self.k.send('SetSpeed100', repeat=2) | |
self.k.send('DeployHardpointToggle') | |
self.k.send('IncreaseEnginesPower', repeat=4) | |
self.k.send('UseBoostJuice') | |
sleep(2) | |
# while the ship is not in supercruise: booster and attempt supercruise | |
# could be mass locked for a bit | |
while self.jn.ship_state()['status'] == 'in_space': | |
if self.voice != None: | |
self.voice.say("Boosting, commanding supercruise") | |
self.k.send('UseBoostJuice') | |
sleep(1) | |
self.k.send('HyperSuperCombination') | |
sleep(9) | |
# in supercruise, wait a bit to get away, then throttle 0 and exit supercruise, full pips to system | |
if self.voice != None: | |
self.voice.say("In supercruise, retreating from site") | |
sleep(1) | |
self.k.send('SetSpeed100', repeat=2) | |
sleep(20) | |
self.k.send('SetSpeedZero') | |
sleep(10) | |
if self.voice != None: | |
self.voice.say("Exiting supercruise, all power to system and weapons") | |
self.k.send('HyperSuperCombination', repeat=2) | |
sleep(7) | |
self.k.send('IncreaseSystemsPower', repeat=3) | |
self.k.send('IncreaseWeaponsPower', repeat=3) | |
def launch_fighter(self): | |
# menu select figher deploy | |
if self.voice != None: | |
self.voice.say("Deploying fighter") | |
self.k.send('HeadLookReset') | |
self.k.send('UIFocus', state=1) | |
self.k.send('UI_Down') | |
self.k.send('UIFocus', state=0) | |
sleep(0.2) | |
# Reset to top, go left x2, go up x3 | |
self.k.send('UI_Left', repeat=2) | |
self.k.send('UI_Up', repeat=3) | |
sleep(0.1) | |
# Down to fighter and then Right then will be over deploy button | |
self.k.send('UI_Down') | |
self.k.send('UI_Right') | |
sleep(0.1) | |
# go to top fighter bay selection | |
self.k.send('UI_Up') | |
# toggle between fighter bays, if a fighter gets restored, that bay is busy | |
# rebuilding so need to us the other one | |
if self.fighter_bay == 2: | |
self.k.send('UI_Down') | |
self.fighter_bay = 1 | |
else: | |
self.fighter_bay = 2 | |
# Deploy should be highlighted, Select it | |
self.k.send('UI_Select') | |
# select which pilot | |
self.k.send('UI_Up', repeat=4) # ensure top entry selected | |
self.k.send('UI_Down') # go down one and select the hired pilot | |
self.k.send('UI_Select') | |
sleep(0.2) | |
self.k.send('UI_Back') | |
self.k.send('HeadLookReset') | |
# | |
# Command fighter to attack at will | |
# self.k.send().... ??OrderAggressiveBehaviour | |
self.k.send('SetSpeedZero') | |
""" | |
Uncomment this whole block below to run this standalone: >python afk-combat.py | |
Hotkeys: | |
Alt + q - quit program | |
Alt + s - force shield down event | |
Alt + f - force fighter destroy event | |
Alt + d - dump the states | |
""" | |
"""Remove this line and last line at button to uncomment the blow of code below | |
inp = None | |
def key_callback(name): | |
global inp | |
inp = name | |
def main(): | |
global inp | |
k = EDKeys() | |
jn = EDJournal() | |
v = Voice() | |
v.set_on() | |
afk = AFK_Combat(k, jn, v) | |
v.say(" AFK Combat Assist active") | |
print("Hot keys\n alt+q = Quit\n alt+s = force shield down event\n alt+f = force fighter destroy event\n alt+d = dump state\n") | |
# trap specific hotkeys | |
keyboard.add_hotkey('alt+q', key_callback, args =('q')) | |
keyboard.add_hotkey('alt+s', key_callback, args =('s')) | |
keyboard.add_hotkey('alt+f', key_callback, args =('f')) | |
keyboard.add_hotkey('alt+d', key_callback, args =('d')) | |
while True: | |
inp = None | |
sleep(2) | |
if inp == 'q': | |
break | |
if inp == 'd': | |
print (jn.ship_state()) | |
if afk.check_shields_up() == False or inp == 's': | |
afk.set_focus_elite_window() | |
v.say("Shields down, evading") | |
afk.evade() | |
# after supercruise the menu is reset to top | |
afk.launch_fighter() # at new location launch fighter | |
break | |
if afk.check_fighter_destroyed() == True or inp == 'f': | |
afk.set_focus_elite_window() | |
v.say("Fighter Destroyed, redeploying") | |
afk.launch_fighter() # assuming two fighter bays | |
v.say("Terminating AFK Combat Assist") | |
sleep(1) | |
v.quit() | |
if __name__ == "__main__": | |
main() | |
""" | |
--- | |
File: /EDAP_data.py | |
--- | |
""" | |
Static data. | |
For easy reference any variable should be prefixed with the name of the file it | |
was either in originally, or where the primary code utilising it is. | |
Note: Some of this file comes from EDMarketConnector's edmc_data.py file | |
(https://github.com/EDCD/EDMarketConnector/blob/main/edmc_data.py). | |
""" | |
# Status.json / Dashboard Flags constants | |
FlagsDocked = 1 << 0 # on a landing pad in space or planet | |
FlagsLanded = 1 << 1 # on planet surface (not planet landing pad) | |
FlagsLandingGearDown = 1 << 2 | |
FlagsShieldsUp = 1 << 3 | |
FlagsSupercruise = 1 << 4 # While in super-cruise | |
FlagsFlightAssistOff = 1 << 5 | |
FlagsHardpointsDeployed = 1 << 6 | |
FlagsInWing = 1 << 7 | |
FlagsLightsOn = 1 << 8 | |
FlagsCargoScoopDeployed = 1 << 9 | |
FlagsSilentRunning = 1 << 10 | |
FlagsScoopingFuel = 1 << 11 | |
FlagsSrvHandbrake = 1 << 12 | |
FlagsSrvTurret = 1 << 13 # using turret view | |
FlagsSrvUnderShip = 1 << 14 # turret retracted | |
FlagsSrvDriveAssist = 1 << 15 | |
FlagsFsdMassLocked = 1 << 16 | |
FlagsFsdCharging = 1 << 17 # While charging and jumping for super-cruise or system jump | |
FlagsFsdCooldown = 1 << 18 # Following super-cruise or jump | |
FlagsLowFuel = 1 << 19 # < 25% | |
FlagsOverHeating = 1 << 20 # > 100%, or is this 80% now ? | |
FlagsHasLatLong = 1 << 21 # On when altimeter is visible (either OC/DRP mode or 2Km/SURF mode). | |
FlagsIsInDanger = 1 << 22 | |
FlagsBeingInterdicted = 1 << 23 | |
FlagsInMainShip = 1 << 24 | |
FlagsInFighter = 1 << 25 | |
FlagsInSRV = 1 << 26 | |
FlagsAnalysisMode = 1 << 27 # Hud in Analysis mode | |
FlagsNightVision = 1 << 28 | |
FlagsAverageAltitude = 1 << 29 # Altitude from Average radius. On when altimeter shows OC/DRP, Off if altimeter is not shown or showing 2Km/SURF. | |
FlagsFsdJump = 1 << 30 # While jumping to super-cruise or system jump. See also Flags2FsdHyperdriveCharging. | |
FlagsSrvHighBeam = 1 << 31 | |
# Status.json / Dashboard Flags2 constants | |
Flags2OnFoot = 1 << 0 | |
Flags2InTaxi = 1 << 1 # (or dropship/shuttle) | |
Flags2InMulticrew = 1 << 2 # (ie in someone else’s ship) | |
Flags2OnFootInStation = 1 << 3 | |
Flags2OnFootOnPlanet = 1 << 4 | |
Flags2AimDownSight = 1 << 5 | |
Flags2LowOxygen = 1 << 6 | |
Flags2LowHealth = 1 << 7 | |
Flags2Cold = 1 << 8 | |
Flags2Hot = 1 << 9 | |
Flags2VeryCold = 1 << 10 | |
Flags2VeryHot = 1 << 11 | |
Flags2GlideMode = 1 << 12 | |
Flags2OnFootInHangar = 1 << 13 | |
Flags2OnFootSocialSpace = 1 << 14 | |
Flags2OnFootExterior = 1 << 15 | |
Flags2BreathableAtmosphere = 1 << 16 | |
Flags2TelepresenceMulticrew = 1 << 17 | |
Flags2PhysicalMulticrew = 1 << 18 | |
Flags2FsdHyperdriveCharging = 1 << 19 # While charging and jumping for system jump | |
Flags2FsdScoActive = 1 << 20 | |
Flags2Future21 = 1 << 21 | |
Flags2Future22 = 1 << 22 | |
Flags2Future23 = 1 << 23 | |
Flags2Future24 = 1 << 24 | |
Flags2Future25 = 1 << 25 | |
Flags2Future26 = 1 << 26 | |
Flags2Future27 = 1 << 27 | |
Flags2Future28 = 1 << 28 | |
Flags2Future29 = 1 << 29 | |
Flags2Future30 = 1 << 30 | |
Flags2Future31 = 1 << 31 | |
# Status.json Dashboard GuiFocus constants | |
GuiFocusNoFocus = 0 # ship view | |
GuiFocusInternalPanel = 1 # right hand side | |
GuiFocusExternalPanel = 2 # left hand (nav) panel | |
GuiFocusCommsPanel = 3 # top | |
GuiFocusRolePanel = 4 # bottom | |
GuiFocusStationServices = 5 | |
GuiFocusGalaxyMap = 6 | |
GuiFocusSystemMap = 7 | |
GuiFocusOrrery = 8 | |
GuiFocusFSS = 9 | |
GuiFocusSAA = 10 | |
GuiFocusCodex = 11 | |
# Journal.log Ship Name constants | |
ship_name_map = { | |
'adder': 'Adder', | |
'anaconda': 'Anaconda', | |
'asp': 'Asp Explorer', | |
'asp_scout': 'Asp Scout', | |
'belugaliner': 'Beluga Liner', | |
'cobramkiii': 'Cobra Mk III', | |
'cobramkiv': 'Cobra Mk IV', | |
'cobramkv': 'Cobra Mk V', | |
'corsair': 'Corsair', | |
'clipper': 'Panther Clipper', | |
'cutter': 'Imperial Cutter', | |
'diamondback': 'Diamondback Scout', | |
'diamondbackxl': 'Diamondback Explorer', | |
'dolphin': 'Dolphin', | |
'eagle': 'Eagle', | |
'empire_courier': 'Imperial Courier', | |
'empire_eagle': 'Imperial Eagle', | |
'empire_fighter': 'Imperial Fighter', | |
'empire_trader': 'Imperial Clipper', | |
'federation_corvette': 'Federal Corvette', | |
'federation_dropship': 'Federal Dropship', | |
'federation_dropship_mkii': 'Federal Assault Ship', | |
'federation_gunship': 'Federal Gunship', | |
'federation_fighter': 'F63 Condor', | |
'ferdelance': 'Fer-de-Lance', | |
'hauler': 'Hauler', | |
'independant_trader': 'Keelback', | |
'independent_fighter': 'Taipan Fighter', | |
'krait_mkii': 'Krait Mk II', | |
'krait_light': 'Krait Phantom', | |
'mamba': 'Mamba', | |
'mandalay': 'Mandalay', | |
'orca': 'Orca', | |
'python': 'Python', | |
'python_nx': 'Python Mk II', | |
'scout': 'Taipan Fighter', | |
'sidewinder': 'Sidewinder', | |
'testbuggy': 'Scarab', | |
'type6': 'Type-6 Transporter', | |
'type7': 'Type-7 Transporter', | |
'type8': 'Type-8 Transporter', | |
'type9': 'Type-9 Heavy', | |
'type9_military': 'Type-10 Defender', | |
'typex': 'Alliance Chieftain', | |
'typex_2': 'Alliance Crusader', | |
'typex_3': 'Alliance Challenger', | |
'viper': 'Viper Mk III', | |
'viper_mkiv': 'Viper Mk IV', | |
'vulture': 'Vulture', | |
} | |
# Journal.log Ship Name to size constants | |
ship_size_map = { | |
'adder': 'S', | |
'anaconda': 'L', | |
'asp': 'M', | |
'asp_scout': 'M', | |
'belugaliner': 'L', | |
'cobramkiii': 'S', | |
'cobramkiv': 'S', | |
'cobramkv': 'S', | |
'corsair': 'M', | |
'clipper': '', | |
'cutter': 'L', | |
'diamondback': 'S', | |
'diamondbackxl': 'S', | |
'dolphin': 'S', | |
'eagle': 'S', | |
'empire_courier': 'S', | |
'empire_eagle': 'S', | |
'empire_fighter': '', | |
'empire_trader': 'L', | |
'federation_corvette': 'L', | |
'federation_dropship': 'M', | |
'federation_dropship_mkii': 'M', | |
'federation_gunship': 'M', | |
'federation_fighter': '', | |
'ferdelance': 'M', | |
'hauler': 'S', | |
'independant_trader': 'M', | |
'independent_fighter': '', | |
'krait_mkii': 'M', | |
'krait_light': 'M', | |
'mamba': 'M', | |
'mandalay': 'M', | |
'orca': 'L', | |
'python': 'M', | |
'python_nx': 'M', | |
'scout': '', | |
'sidewinder': 'S', | |
'testbuggy': '', | |
'type6': 'M', | |
'type7': 'L', | |
'type8': 'L', | |
'type9': 'L', | |
'type9_military': 'L', | |
'typex': 'M', | |
'typex_2': 'M', | |
'typex_3': 'M', | |
'viper': 'S', | |
'viper_mkiv': 'S', | |
'vulture': 'S', | |
} | |
--- | |
File: /EDAP_EDMesg_Client.py | |
--- | |
import threading | |
from EDAP_EDMesg_Interface import ( | |
create_edap_client, | |
LaunchAction, | |
LaunchCompleteEvent, | |
) | |
from time import sleep | |
class EDMesgClient: | |
""" EDMesg Client. Allows connection to EDAP and send actions and receive events. """ | |
def __init__(self, ed_ap, cb): | |
self.ap = ed_ap | |
self.ap_ckb = cb | |
self.actions_port = 15570 | |
self.events_port = 15571 | |
self.client = create_edap_client(self.actions_port, self.events_port) # Factory method for EDCoPilot | |
print("Server starting.") | |
self._client_loop_thread = threading.Thread(target=self._client_loop, daemon=True) | |
self._client_loop_thread.start() | |
def _client_loop(self): | |
""" A loop for the client. | |
This runs on a separate thread monitoring communications in the background. """ | |
try: | |
print("Starting client loop.") | |
while True: | |
print("In client loop.") | |
# Check if we received any events from EDAP | |
if not self.client.pending_events.empty(): | |
event = self.client.pending_events.get() | |
if isinstance(event, LaunchCompleteEvent): | |
self._handle_launch_complete() | |
sleep(0.1) | |
except: | |
print("Shutting down client.") | |
finally: | |
self.client.close() | |
def _handle_launch_complete(self): | |
print(f"Handling Event: LaunchCompleteEvent") | |
# Implement the event handling logic here | |
def send_launch_action(self): | |
""" Send an action request to EDAP. """ | |
print("Sending Action: LaunchAction.") | |
self.client.publish(LaunchAction()) | |
# def main(): | |
# client = create_edap_client() # Factory method for EDCoPilot | |
# print("Client starting.") | |
# try: | |
# # Send an action request to EDAP | |
# print("Sending Action: LaunchAction.") | |
# client.publish(LaunchAction()) | |
# | |
# while True: | |
# # Check if we received any events from EDAP | |
# if not client.pending_events.empty(): | |
# event = client.pending_events.get() | |
# if isinstance(event, UndockCompleteEvent): | |
# _handle_undock_complete() | |
# sleep(0.1) | |
# except KeyboardInterrupt: | |
# print("Shutting down client.") | |
# finally: | |
# client.close() | |
def main(): | |
edmesg_client = EDMesgClient(ed_ap=None, cb=None) | |
while 1: # not edmesg_client.stop: | |
sleep(1) | |
if __name__ == "__main__": | |
main() | |
--- | |
File: /EDAP_EDMesg_Interface.py | |
--- | |
from EDMesg.EDMesgBase import EDMesgAction, EDMesgEvent | |
from EDMesg.EDMesgProvider import EDMesgProvider | |
from EDMesg.EDMesgClient import EDMesgClient | |
class GetEDAPLocationAction(EDMesgAction): | |
pass | |
class LoadWaypointFileAction(EDMesgAction): | |
filepath: str | |
class StartWaypointAssistAction(EDMesgAction): | |
pass | |
class StopAllAssistsAction(EDMesgAction): | |
pass | |
class LaunchAction(EDMesgAction): | |
pass | |
class SystemMapTargetStationByBookmarkAction(EDMesgAction): | |
type: str | |
number: int | |
class GalaxyMapTargetStationByBookmarkAction(EDMesgAction): | |
type: str | |
number: int | |
class GalaxyMapTargetSystemByNameAction(EDMesgAction): | |
name: str | |
class EDAPLocationEvent(EDMesgEvent): | |
path: str | |
class LaunchCompleteEvent(EDMesgEvent): | |
pass | |
# Factory methods | |
provider_name = "EDAP" | |
actions: list[type[EDMesgAction]] = [ | |
GetEDAPLocationAction, | |
LoadWaypointFileAction, | |
LaunchAction, | |
StartWaypointAssistAction, | |
StopAllAssistsAction, | |
SystemMapTargetStationByBookmarkAction, | |
GalaxyMapTargetStationByBookmarkAction, | |
GalaxyMapTargetSystemByNameAction, | |
] | |
events: list[type[EDMesgEvent]] = [ | |
EDAPLocationEvent, | |
LaunchCompleteEvent, | |
] | |
#actions_port = 15570 | |
#events_port = 15571 | |
def create_edap_provider(actions_port: int, events_port: int) -> EDMesgProvider: | |
return EDMesgProvider( | |
provider_name=provider_name, | |
action_types=actions, | |
event_types=events, | |
action_port=actions_port, | |
event_port=events_port, | |
) | |
def create_edap_client(actions_port: int, events_port: int) -> EDMesgClient: | |
return EDMesgClient( | |
provider_name=provider_name, | |
action_types=actions, | |
event_types=events, | |
action_port=actions_port, | |
event_port=events_port, | |
) | |
--- | |
File: /EDAP_EDMesg_Server.py | |
--- | |
import os | |
import sys | |
import threading | |
from EDMesg.EDMesgBase import EDMesgWelcomeAction | |
from EDAP_EDMesg_Interface import ( | |
create_edap_provider, | |
GetEDAPLocationAction, | |
LoadWaypointFileAction, | |
StartWaypointAssistAction, | |
StopAllAssistsAction, | |
LaunchAction, | |
SystemMapTargetStationByBookmarkAction, | |
GalaxyMapTargetStationByBookmarkAction, | |
GalaxyMapTargetSystemByNameAction, | |
EDAPLocationEvent, | |
LaunchCompleteEvent, | |
) | |
from time import sleep | |
class EDMesgServer: | |
""" EDMesg Server. Allows EDMesg Clients to connect to EDAP and send actions and receive events. | |
https://github.com/RatherRude/EDMesg | |
""" | |
def __init__(self, ed_ap, cb): | |
self.ap = ed_ap | |
self.ap_ckb = cb | |
self.actions_port = 0 | |
self.events_port = 0 | |
self._provider = None | |
self._server_loop_thread = None | |
def start_server(self): | |
try: | |
self._provider = create_edap_provider(self.actions_port, self.events_port) # Factory method for EDCoPilot | |
self.ap_ckb('log', "Starting EDMesg Server.") | |
print("Server starting.") | |
self._server_loop_thread = threading.Thread(target=self._server_loop, daemon=True) | |
self._server_loop_thread.start() | |
except Exception as e: | |
self.ap_ckb('log', f"EDMesg Server failed to start: {e}") | |
print(f"EDMesg Server failed to start: {e}") | |
def _server_loop(self): | |
""" A loop for the server. | |
This runs on a separate thread monitoring communications in the background. """ | |
try: | |
while True: | |
# Check if we received an action from the client | |
if not self._provider.pending_actions.empty(): | |
action = self._provider.pending_actions.get() | |
if isinstance(action, EDMesgWelcomeAction): | |
print("new client connected") | |
if isinstance(action, GetEDAPLocationAction): | |
self._get_edap_location(self._provider) | |
if isinstance(action, LoadWaypointFileAction): | |
self._load_waypoint_file(action.filepath) | |
if isinstance(action, StartWaypointAssistAction): | |
self._start_waypoint_assist() | |
if isinstance(action, StopAllAssistsAction): | |
self._stop_all_assists() | |
if isinstance(action, SystemMapTargetStationByBookmarkAction): | |
self._system_map_target_station_by_bookmark(action.type, action.number) | |
if isinstance(action, GalaxyMapTargetStationByBookmarkAction): | |
self._galaxy_map_target_station_by_bookmark(action.type, action.number) | |
if isinstance(action, GalaxyMapTargetSystemByNameAction): | |
self._galaxy_map_target_system_by_name(action.name) | |
if isinstance(action, LaunchAction): | |
self._launch(self._provider) | |
sleep(0.1) | |
except: | |
print("Shutting down provider.") | |
finally: | |
self._provider.close() | |
def _get_edap_location(self, provider): | |
self.ap_ckb('log', "Received EDMesg Action: GetEDAPLocationAction") | |
print("Received EDMesg Action: GetEDAPLocationAction") | |
# Get the absolute path of the running script | |
pathname = os.path.dirname(sys.argv[0]) | |
abspath = os.path.abspath(pathname) | |
print(f"Sending Event: EDAPLocationEvent") | |
provider.publish( | |
EDAPLocationEvent( | |
path=abspath | |
) | |
) | |
def _load_waypoint_file(self, filepath: str): | |
self.ap_ckb('log', "Received EDMesg Action: LoadWaypointFileAction") | |
print("Received EDMesg Action: LoadWaypointFileAction") | |
self.ap.waypoint.load_waypoint_file(filepath) | |
def _start_waypoint_assist(self): | |
self.ap_ckb('log', "Received EDMesg Action: StartWaypointAssistAction") | |
print("Received EDMesg Action: StartWaypointAssistAction") | |
# Start the waypoint assist using callback | |
self.ap_ckb('waypoint_start') | |
def _stop_all_assists(self): | |
self.ap_ckb('log', "Received EDMesg Action: StopAllAssistsAction") | |
print("Received EDMesg Action: StopAllAssistsAction") | |
# Stop all assists using callback | |
self.ap_ckb('stop_all_assists') | |
def _launch(self, provider): | |
self.ap_ckb('log', "Received EDMesg Action: LaunchAction") | |
print("Received EDMesg Action: LaunchAction") | |
# Request | |
#self.ap.waypoint_undock_seq() | |
sleep(1) | |
self.ap_ckb('log', "Sending EDMesg Event: LaunchCompleteEvent") | |
print(f"Sending Event: LaunchCompleteEvent") | |
provider.publish( | |
LaunchCompleteEvent() | |
) | |
def _system_map_target_station_by_bookmark(self, bm_type: str, number: int): | |
self.ap_ckb('log', "Received EDMesg Action: SystemMapTargetStationByBookmarkAction") | |
self.ap.system_map.goto_system_map() | |
self.ap.system_map.set_sys_map_dest_bookmark(self.ap, bm_type, number) | |
def _galaxy_map_target_station_by_bookmark(self, bm_type: str, number: int): | |
self.ap_ckb('log', "Received EDMesg Action: GalaxyMapTargetStationByBookmarkAction") | |
self.ap.galaxy_map.goto_galaxy_map() | |
self.ap.galaxy_map.set_gal_map_dest_bookmark(self.ap, bm_type, number) | |
def _galaxy_map_target_system_by_name(self, name: str): | |
self.ap_ckb('log', "Received EDMesg Action: GalaxyMapTargetSystemByNameAction") | |
self.ap.galaxy_map.goto_galaxy_map() | |
self.ap.galaxy_map.set_gal_map_destination_text(self.ap, name, target_select_cb=None) | |
def main(): | |
edmesg_server = EDMesgServer(ed_ap=None, cb=None) | |
# while not edmesg_server.stop: | |
# sleep(1) | |
# def main(): | |
# provider = create_edap_provider() # Factory method for EDCoPilot | |
# print("Server starting.") | |
# try: | |
# while True: | |
# # Check if we received an action from the client | |
# if not provider.pending_actions.empty(): | |
# action = provider.pending_actions.get() | |
# if isinstance(action, EDMesgWelcomeAction): | |
# print("new client connected") | |
# if isinstance(action, LoadWaypointFileAction): | |
# load_waypoint_file(action.filepath) | |
# if isinstance(action, StartWaypointAssistAction): | |
# start_waypoint_assist() | |
# if isinstance(action, StopAllAssistsAction): | |
# stop_all_assists() | |
# if isinstance(action, LaunchAction): | |
# undock(provider) | |
# | |
# sleep(0.1) | |
# except KeyboardInterrupt: | |
# print("Shutting down provider.") | |
# finally: | |
# provider.close() | |
if __name__ == "__main__": | |
main() | |
--- | |
File: /EDAPGui.py | |
--- | |
import queue | |
import sys | |
import os | |
import threading | |
import kthread | |
from datetime import datetime | |
from time import sleep | |
import cv2 | |
import json | |
from pathlib import Path | |
import keyboard | |
import webbrowser | |
import requests | |
from PIL import Image, ImageGrab, ImageTk | |
import tkinter as tk | |
from tkinter import * | |
from tkinter import filedialog as fd | |
from tkinter import messagebox | |
from tkinter import ttk | |
from idlelib.tooltip import Hovertip | |
from Voice import * | |
from MousePt import MousePoint | |
from Image_Templates import * | |
from Screen import * | |
from Screen_Regions import * | |
from EDKeys import * | |
from EDJournal import * | |
from ED_AP import * | |
from EDlogger import logger | |
""" | |
File:EDAPGui.py | |
Description: | |
User interface for controlling the ED Autopilot | |
Note: | |
Ideas taken from: https://github.com/skai2/EDAutopilot | |
HotKeys: | |
Home - Start FSD Assist | |
INS - Start SC Assist | |
PG UP - Start Robigo Assist | |
End - Terminate any ongoing assist (FSD, SC, AFK) | |
Author: [email protected] | |
""" | |
# --------------------------------------------------------------------------- | |
# must be updated with a new release so that the update check works properly! | |
# contains the names of the release. | |
EDAP_VERSION = "V1.4.1" | |
# depending on how release versions are best marked you could also change it to the release tag, see function check_update. | |
# --------------------------------------------------------------------------- | |
FORM_TYPE_CHECKBOX = 0 | |
FORM_TYPE_SPINBOX = 1 | |
FORM_TYPE_ENTRY = 2 | |
def hyperlink_callback(url): | |
webbrowser.open_new(url) | |
class APGui(): | |
def __init__(self, root): | |
self.root = root | |
root.title("EDAutopilot " + EDAP_VERSION) | |
# root.overrideredirect(True) | |
# root.geometry("400x550") | |
# root.configure(bg="blue") | |
root.protocol("WM_DELETE_WINDOW", self.close_window) | |
root.resizable(False, False) | |
self.tooltips = { | |
'FSD Route Assist': "Will execute your route. \nAt each jump the sequence will perform some fuel scooping.", | |
'Supercruise Assist': "Will keep your ship pointed to target, \nyou target can only be a station for the autodocking to work.", | |
'Waypoint Assist': "When selected, will prompt for the waypoint file. \nThe waypoint file contains System names that \nwill be entered into Galaxy Map and route plotted.", | |
'Robigo Assist': "", | |
'DSS Assist': "When selected, will perform DSS scans while you are traveling between stars.", | |
'Single Waypoint Assist': "", | |
'ELW Scanner': "Will perform FSS scans while FSD Assist is traveling between stars. \nIf the FSS shows a signal in the region of Earth, \nWater or Ammonia type worlds, it will announce that discovery.", | |
'AFK Combat Assist': "Used with a AFK Combat ship in a Rez Zone.", | |
'RollRate': "Roll rate your ship has in deg/sec. Higher the number the more maneuverable the ship.", | |
'PitchRate': "Pitch (up/down) rate your ship has in deg/sec. Higher the number the more maneuverable the ship.", | |
'YawRate': "Yaw rate (rudder) your ship has in deg/sec. Higher the number the more maneuverable the ship.", | |
'SunPitchUp+Time': "This field are for ship that tend to overheat. \nProviding 1-2 more seconds of Pitch up when avoiding the Sun \nwill overcome this problem.", | |
'Sun Bright Threshold': "The low level for brightness detection, \nrange 0-255, want to mask out darker items", | |
'Nav Align Tries': "How many attempts the ap should make at alignment.", | |
'Jump Tries': "How many attempts the ap should make to jump.", | |
'Docking Retries': "How many attempts to make to dock.", | |
'Wait For Autodock': "After docking granted, \nwait this amount of time for us to get docked with autodocking", | |
'Start FSD': "Button to start FSD route assist.", | |
'Start SC': "Button to start Supercruise assist.", | |
'Start Robigo': "Button to start Robigo assist.", | |
'Stop All': "Button to stop all assists.", | |
'Refuel Threshold': "If fuel level get below this level, \nit will attempt refuel.", | |
'Scoop Timeout': "Number of second to wait for full tank, \nmight mean we are not scooping well or got a small scooper", | |
'Fuel Threshold Abort': "Level at which AP will terminate, \nbecause we are not scooping well.", | |
'X Offset': "Offset left the screen to start place overlay text.", | |
'Y Offset': "Offset down the screen to start place overlay text.", | |
'Font Size': "Font size of the overlay.", | |
'Ship Config Button': "Read in a file with roll, pitch, yaw values for a ship.", | |
'Calibrate': "Will iterate through a set of scaling values \ngetting the best match for your system. \nSee HOWTO-Calibrate.md", | |
'Waypoint List Button': "Read in a file with with your Waypoints.", | |
'Cap Mouse XY': "This will provide the StationCoord value of the Station in the SystemMap. \nSelecting this button and then clicking on the Station in the SystemMap \nwill return the x,y value that can be pasted in the waypoints file", | |
'Reset Waypoint List': "Reset your waypoint list, \nthe waypoint assist will start again at the first point in the list." | |
} | |
self.gui_loaded = False | |
self.log_buffer = queue.Queue() | |
self.ed_ap = EDAutopilot(cb=self.callback) | |
self.ed_ap.robigo.set_single_loop(self.ed_ap.config['Robigo_Single_Loop']) | |
self.mouse = MousePoint() | |
self.checkboxvar = {} | |
self.radiobuttonvar = {} | |
self.entries = {} | |
self.lab_ck = {} | |
self.single_waypoint_system = StringVar() | |
self.single_waypoint_station = StringVar() | |
self.TCE_Destination_Filepath = StringVar() | |
self.FSD_A_running = False | |
self.SC_A_running = False | |
self.WP_A_running = False | |
self.RO_A_running = False | |
self.DSS_A_running = False | |
self.SWP_A_running = False | |
self.cv_view = False | |
self.TCE_Destination_Filepath.set(self.ed_ap.config['TCEDestinationFilepath']) | |
self.msgList = self.gui_gen(root) | |
self.checkboxvar['Enable Randomness'].set(self.ed_ap.config['EnableRandomness']) | |
self.checkboxvar['Activate Elite for each key'].set(self.ed_ap.config['ActivateEliteEachKey']) | |
self.checkboxvar['Automatic logout'].set(self.ed_ap.config['AutomaticLogout']) | |
self.checkboxvar['Enable Overlay'].set(self.ed_ap.config['OverlayTextEnable']) | |
self.checkboxvar['Enable Voice'].set(self.ed_ap.config['VoiceEnable']) | |
self.radiobuttonvar['dss_button'].set(self.ed_ap.config['DSSButton']) | |
self.entries['ship']['PitchRate'].delete(0, END) | |
self.entries['ship']['RollRate'].delete(0, END) | |
self.entries['ship']['YawRate'].delete(0, END) | |
self.entries['ship']['SunPitchUp+Time'].delete(0, END) | |
self.entries['autopilot']['Sun Bright Threshold'].delete(0, END) | |
self.entries['autopilot']['Nav Align Tries'].delete(0, END) | |
self.entries['autopilot']['Jump Tries'].delete(0, END) | |
self.entries['autopilot']['Docking Retries'].delete(0, END) | |
self.entries['autopilot']['Wait For Autodock'].delete(0, END) | |
self.entries['refuel']['Refuel Threshold'].delete(0, END) | |
self.entries['refuel']['Scoop Timeout'].delete(0, END) | |
self.entries['refuel']['Fuel Threshold Abort'].delete(0, END) | |
self.entries['overlay']['X Offset'].delete(0, END) | |
self.entries['overlay']['Y Offset'].delete(0, END) | |
self.entries['overlay']['Font Size'].delete(0, END) | |
self.entries['buttons']['Start FSD'].delete(0, END) | |
self.entries['buttons']['Start SC'].delete(0, END) | |
self.entries['buttons']['Start Robigo'].delete(0, END) | |
self.entries['buttons']['Stop All'].delete(0, END) | |
self.entries['ship']['PitchRate'].insert(0, float(self.ed_ap.pitchrate)) | |
self.entries['ship']['RollRate'].insert(0, float(self.ed_ap.rollrate)) | |
self.entries['ship']['YawRate'].insert(0, float(self.ed_ap.yawrate)) | |
self.entries['ship']['SunPitchUp+Time'].insert(0, float(self.ed_ap.sunpitchuptime)) | |
self.entries['autopilot']['Sun Bright Threshold'].insert(0, int(self.ed_ap.config['SunBrightThreshold'])) | |
self.entries['autopilot']['Nav Align Tries'].insert(0, int(self.ed_ap.config['NavAlignTries'])) | |
self.entries['autopilot']['Jump Tries'].insert(0, int(self.ed_ap.config['JumpTries'])) | |
self.entries['autopilot']['Docking Retries'].insert(0, int(self.ed_ap.config['DockingRetries'])) | |
self.entries['autopilot']['Wait For Autodock'].insert(0, int(self.ed_ap.config['WaitForAutoDockTimer'])) | |
self.entries['refuel']['Refuel Threshold'].insert(0, int(self.ed_ap.config['RefuelThreshold'])) | |
self.entries['refuel']['Scoop Timeout'].insert(0, int(self.ed_ap.config['FuelScoopTimeOut'])) | |
self.entries['refuel']['Fuel Threshold Abort'].insert(0, int(self.ed_ap.config['FuelThreasholdAbortAP'])) | |
self.entries['overlay']['X Offset'].insert(0, int(self.ed_ap.config['OverlayTextXOffset'])) | |
self.entries['overlay']['Y Offset'].insert(0, int(self.ed_ap.config['OverlayTextYOffset'])) | |
self.entries['overlay']['Font Size'].insert(0, int(self.ed_ap.config['OverlayTextFontSize'])) | |
self.entries['buttons']['Start FSD'].insert(0, str(self.ed_ap.config['HotKey_StartFSD'])) | |
self.entries['buttons']['Start SC'].insert(0, str(self.ed_ap.config['HotKey_StartSC'])) | |
self.entries['buttons']['Start Robigo'].insert(0, str(self.ed_ap.config['HotKey_StartRobigo'])) | |
self.entries['buttons']['Stop All'].insert(0, str(self.ed_ap.config['HotKey_StopAllAssists'])) | |
if self.ed_ap.config['LogDEBUG']: | |
self.radiobuttonvar['debug_mode'].set("Debug") | |
elif self.ed_ap.config['LogINFO']: | |
self.radiobuttonvar['debug_mode'].set("Info") | |
else: | |
self.radiobuttonvar['debug_mode'].set("Error") | |
# global trap for these keys, the 'end' key will stop any current AP action | |
# the 'home' key will start the FSD Assist. May want another to start SC Assist | |
keyboard.add_hotkey(self.ed_ap.config['HotKey_StopAllAssists'], self.stop_all_assists) | |
keyboard.add_hotkey(self.ed_ap.config['HotKey_StartFSD'], self.callback, args=('fsd_start', None)) | |
keyboard.add_hotkey(self.ed_ap.config['HotKey_StartSC'], self.callback, args=('sc_start', None)) | |
keyboard.add_hotkey(self.ed_ap.config['HotKey_StartRobigo'], self.callback, args=('robigo_start', None)) | |
# check for updates | |
self.check_updates() | |
self.ed_ap.gui_loaded = True | |
self.gui_loaded = True | |
# Send a log entry which will flush out the buffer. | |
self.callback('log', 'ED Autopilot loaded successfully.') | |
# callback from the EDAP, to configure GUI items | |
def callback(self, msg, body=None): | |
if msg == 'log': | |
self.log_msg(body) | |
elif msg == 'log+vce': | |
self.log_msg(body) | |
self.ed_ap.vce.say(body) | |
elif msg == 'statusline': | |
self.update_statusline(body) | |
elif msg == 'fsd_stop': | |
logger.debug("Detected 'fsd_stop' callback msg") | |
self.checkboxvar['FSD Route Assist'].set(0) | |
self.check_cb('FSD Route Assist') | |
elif msg == 'fsd_start': | |
self.checkboxvar['FSD Route Assist'].set(1) | |
self.check_cb('FSD Route Assist') | |
elif msg == 'sc_stop': | |
logger.debug("Detected 'sc_stop' callback msg") | |
self.checkboxvar['Supercruise Assist'].set(0) | |
self.check_cb('Supercruise Assist') | |
elif msg == 'sc_start': | |
self.checkboxvar['Supercruise Assist'].set(1) | |
self.check_cb('Supercruise Assist') | |
elif msg == 'waypoint_stop': | |
logger.debug("Detected 'waypoint_stop' callback msg") | |
self.checkboxvar['Waypoint Assist'].set(0) | |
self.check_cb('Waypoint Assist') | |
elif msg == 'waypoint_start': | |
self.checkboxvar['Waypoint Assist'].set(1) | |
self.check_cb('Waypoint Assist') | |
elif msg == 'robigo_stop': | |
logger.debug("Detected 'robigo_stop' callback msg") | |
self.checkboxvar['Robigo Assist'].set(0) | |
self.check_cb('Robigo Assist') | |
elif msg == 'robigo_start': | |
self.checkboxvar['Robigo Assist'].set(1) | |
self.check_cb('Robigo Assist') | |
elif msg == 'afk_stop': | |
logger.debug("Detected 'afk_stop' callback msg") | |
self.checkboxvar['AFK Combat Assist'].set(0) | |
self.check_cb('AFK Combat Assist') | |
elif msg == 'dss_start': | |
logger.debug("Detected 'dss_start' callback msg") | |
self.checkboxvar['DSS Assist'].set(1) | |
self.check_cb('DSS Assist') | |
elif msg == 'dss_stop': | |
logger.debug("Detected 'dss_stop' callback msg") | |
self.checkboxvar['DSS Assist'].set(0) | |
self.check_cb('DSS Assist') | |
elif msg == 'single_waypoint_stop': | |
logger.debug("Detected 'single_waypoint_stop' callback msg") | |
self.checkboxvar['Single Waypoint Assist'].set(0) | |
self.check_cb('Single Waypoint Assist') | |
elif msg == 'stop_all_assists': | |
logger.debug("Detected 'stop_all_assists' callback msg") | |
self.checkboxvar['FSD Route Assist'].set(0) | |
self.check_cb('FSD Route Assist') | |
self.checkboxvar['Supercruise Assist'].set(0) | |
self.check_cb('Supercruise Assist') | |
self.checkboxvar['Waypoint Assist'].set(0) | |
self.check_cb('Waypoint Assist') | |
self.checkboxvar['Robigo Assist'].set(0) | |
self.check_cb('Robigo Assist') | |
self.checkboxvar['AFK Combat Assist'].set(0) | |
self.check_cb('AFK Combat Assist') | |
self.checkboxvar['DSS Assist'].set(0) | |
self.check_cb('DSS Assist') | |
self.checkboxvar['Single Waypoint Assist'].set(0) | |
self.check_cb('Single Waypoint Assist') | |
elif msg == 'jumpcount': | |
self.update_jumpcount(body) | |
elif msg == 'update_ship_cfg': | |
self.update_ship_cfg() | |
def update_ship_cfg(self): | |
# load up the display with what we read from ED_AP for the current ship | |
self.entries['ship']['PitchRate'].delete(0, END) | |
self.entries['ship']['RollRate'].delete(0, END) | |
self.entries['ship']['YawRate'].delete(0, END) | |
self.entries['ship']['SunPitchUp+Time'].delete(0, END) | |
self.entries['ship']['PitchRate'].insert(0, self.ed_ap.pitchrate) | |
self.entries['ship']['RollRate'].insert(0, self.ed_ap.rollrate) | |
self.entries['ship']['YawRate'].insert(0, self.ed_ap.yawrate) | |
self.entries['ship']['SunPitchUp+Time'].insert(0, self.ed_ap.sunpitchuptime) | |
def calibrate_callback(self): | |
self.ed_ap.calibrate() | |
def calibrate_compass_callback(self): | |
self.ed_ap.calibrate_compass() | |
def quit(self): | |
logger.debug("Entered: quit") | |
self.close_window() | |
def close_window(self): | |
logger.debug("Entered: close_window") | |
self.stop_fsd() | |
self.stop_sc() | |
self.ed_ap.quit() | |
sleep(0.1) | |
self.root.destroy() | |
# this routine is to stop any current autopilot activity | |
def stop_all_assists(self): | |
logger.debug("Entered: stop_all_assists") | |
self.callback('stop_all_assists') | |
def start_fsd(self): | |
logger.debug("Entered: start_fsd") | |
self.ed_ap.set_fsd_assist(True) | |
self.FSD_A_running = True | |
self.log_msg("FSD Route Assist start") | |
self.ed_ap.vce.say("FSD Route Assist On") | |
def stop_fsd(self): | |
logger.debug("Entered: stop_fsd") | |
self.ed_ap.set_fsd_assist(False) | |
self.FSD_A_running = False | |
self.log_msg("FSD Route Assist stop") | |
self.ed_ap.vce.say("FSD Route Assist Off") | |
self.update_statusline("Idle") | |
def start_sc(self): | |
logger.debug("Entered: start_sc") | |
self.ed_ap.set_sc_assist(True) | |
self.SC_A_running = True | |
self.log_msg("SC Assist start") | |
self.ed_ap.vce.say("Supercruise Assist On") | |
def stop_sc(self): | |
logger.debug("Entered: stop_sc") | |
self.ed_ap.set_sc_assist(False) | |
self.SC_A_running = False | |
self.log_msg("SC Assist stop") | |
self.ed_ap.vce.say("Supercruise Assist Off") | |
self.update_statusline("Idle") | |
def start_waypoint(self): | |
logger.debug("Entered: start_waypoint") | |
self.ed_ap.set_waypoint_assist(True) | |
self.WP_A_running = True | |
self.log_msg("Waypoint Assist start") | |
self.ed_ap.vce.say("Waypoint Assist On") | |
def stop_waypoint(self): | |
logger.debug("Entered: stop_waypoint") | |
self.ed_ap.set_waypoint_assist(False) | |
self.WP_A_running = False | |
self.log_msg("Waypoint Assist stop") | |
self.ed_ap.vce.say("Waypoint Assist Off") | |
self.update_statusline("Idle") | |
def start_robigo(self): | |
logger.debug("Entered: start_robigo") | |
self.ed_ap.set_robigo_assist(True) | |
self.RO_A_running = True | |
self.log_msg("Robigo Assist start") | |
self.ed_ap.vce.say("Robigo Assist On") | |
def stop_robigo(self): | |
logger.debug("Entered: stop_robigo") | |
self.ed_ap.set_robigo_assist(False) | |
self.RO_A_running = False | |
self.log_msg("Robigo Assist stop") | |
self.ed_ap.vce.say("Robigo Assist Off") | |
self.update_statusline("Idle") | |
def start_dss(self): | |
logger.debug("Entered: start_dss") | |
self.ed_ap.set_dss_assist(True) | |
self.DSS_A_running = True | |
self.log_msg("DSS Assist start") | |
self.ed_ap.vce.say("DSS Assist On") | |
def stop_dss(self): | |
logger.debug("Entered: stop_dss") | |
self.ed_ap.set_dss_assist(False) | |
self.DSS_A_running = False | |
self.log_msg("DSS Assist stop") | |
self.ed_ap.vce.say("DSS Assist Off") | |
self.update_statusline("Idle") | |
def start_single_waypoint_assist(self): | |
""" The debug command to go to a system or station or both.""" | |
logger.debug("Entered: start_single_waypoint_assist") | |
system = self.single_waypoint_system.get() | |
station = self.single_waypoint_station.get() | |
if system != "" or station != "": | |
self.ed_ap.set_single_waypoint_assist(system, station, True) | |
self.SWP_A_running = True | |
self.log_msg("Single Waypoint Assist start") | |
self.ed_ap.vce.say("Single Waypoint Assist On") | |
def stop_single_waypoint_assist(self): | |
""" The debug command to go to a system or station or both.""" | |
logger.debug("Entered: stop_single_waypoint_assist") | |
self.ed_ap.set_single_waypoint_assist("", "", False) | |
self.SWP_A_running = False | |
self.log_msg("Single Waypoint Assist stop") | |
self.ed_ap.vce.say("Single Waypoint Assist Off") | |
self.update_statusline("Idle") | |
def about(self): | |
webbrowser.open_new("https://github.com/SumZer0-git/EDAPGui") | |
def check_updates(self): | |
# response = requests.get("https://api.github.com/repos/SumZer0-git/EDAPGui/releases/latest") | |
# if EDAP_VERSION != response.json()["name"]: | |
# mb = messagebox.askokcancel("Update Check", "A new release version is available. Download now?") | |
# if mb == True: | |
# webbrowser.open_new("https://github.com/SumZer0-git/EDAPGui/releases/latest") | |
pass | |
def open_changelog(self): | |
webbrowser.open_new("https://github.com/SumZer0-git/EDAPGui/blob/main/ChangeLog.md") | |
def open_discord(self): | |
webbrowser.open_new("https://discord.gg/HCgkfSc") | |
def open_logfile(self): | |
os.startfile('autopilot.log') | |
def log_msg(self, msg): | |
message = datetime.now().strftime("%H:%M:%S: ") + msg | |
if not self.gui_loaded: | |
# Store message in queue | |
self.log_buffer.put(message) | |
logger.info(msg) | |
else: | |
# Add queued messages to the list | |
while not self.log_buffer.empty(): | |
self.msgList.insert(END, self.log_buffer.get()) | |
self.msgList.insert(END, message) | |
self.msgList.yview(END) | |
logger.info(msg) | |
def set_statusbar(self, txt): | |
self.statusbar.configure(text=txt) | |
def update_jumpcount(self, txt): | |
self.jumpcount.configure(text=txt) | |
def update_statusline(self, txt): | |
self.status.configure(text="Status: " + txt) | |
self.log_msg(f"Status update: {txt}") | |
def ship_tst_pitch(self): | |
self.ed_ap.ship_tst_pitch() | |
def ship_tst_roll(self): | |
self.ed_ap.ship_tst_roll() | |
def ship_tst_yaw(self): | |
self.ed_ap.ship_tst_yaw() | |
def open_ship_file(self, filename=None): | |
# if a filename was not provided, then prompt user for one | |
if not filename: | |
filetypes = ( | |
('json files', '*.json'), | |
('All files', '*.*') | |
) | |
filename = fd.askopenfilename( | |
title='Open a file', | |
initialdir='./ships/', | |
filetypes=filetypes) | |
if not filename: | |
return | |
with open(filename, 'r') as json_file: | |
f_details = json.load(json_file) | |
# load up the display with what we read, the pass it along to AP | |
self.entries['ship']['PitchRate'].delete(0, END) | |
self.entries['ship']['RollRate'].delete(0, END) | |
self.entries['ship']['YawRate'].delete(0, END) | |
self.entries['ship']['SunPitchUp+Time'].delete(0, END) | |
self.entries['ship']['PitchRate'].insert(0, f_details['pitchrate']) | |
self.entries['ship']['RollRate'].insert(0, f_details['rollrate']) | |
self.entries['ship']['YawRate'].insert(0, f_details['yawrate']) | |
self.entries['ship']['SunPitchUp+Time'].insert(0, f_details['SunPitchUp+Time']) | |
self.ed_ap.rollrate = float(f_details['rollrate']) | |
self.ed_ap.pitchrate = float(f_details['pitchrate']) | |
self.ed_ap.yawrate = float(f_details['yawrate']) | |
self.ed_ap.sunpitchuptime = float(f_details['SunPitchUp+Time']) | |
self.ship_filelabel.set("loaded: " + Path(filename).name) | |
self.ed_ap.update_config() | |
def open_wp_file(self): | |
filetypes = ( | |
('json files', '*.json'), | |
('All files', '*.*') | |
) | |
filename = fd.askopenfilename(title="Waypoint File", initialdir='./waypoints/', filetypes=filetypes) | |
if filename != "": | |
res = self.ed_ap.waypoint.load_waypoint_file(filename) | |
if res: | |
self.wp_filelabel.set("loaded: " + Path(filename).name) | |
else: | |
self.wp_filelabel.set("<no list loaded>") | |
def reset_wp_file(self): | |
if self.WP_A_running != True: | |
mb = messagebox.askokcancel("Waypoint List Reset", "After resetting the Waypoint List, the Waypoint Assist will start again from the first point in the list at the next start.") | |
if mb == True: | |
self.ed_ap.waypoint.mark_all_waypoints_not_complete() | |
else: | |
mb = messagebox.showerror("Waypoint List Error", "Waypoint Assist must be disabled before you can reset the list.") | |
def save_settings(self): | |
self.entry_update() | |
self.ed_ap.update_config() | |
self.ed_ap.update_ship_configs() | |
def load_tce_dest(self): | |
filename = self.ed_ap.config['TCEDestinationFilepath'] | |
if os.path.exists(filename): | |
with open(filename, 'r') as json_file: | |
f_details = json.load(json_file) | |
self.single_waypoint_system.set(f_details['StarSystem']) | |
self.single_waypoint_station.set(f_details['Station']) | |
# new data was added to a field, re-read them all for simple logic | |
def entry_update(self, event=''): | |
try: | |
self.ed_ap.pitchrate = float(self.entries['ship']['PitchRate'].get()) | |
self.ed_ap.rollrate = float(self.entries['ship']['RollRate'].get()) | |
self.ed_ap.yawrate = float(self.entries['ship']['YawRate'].get()) | |
self.ed_ap.sunpitchuptime = float(self.entries['ship']['SunPitchUp+Time'].get()) | |
self.ed_ap.config['SunBrightThreshold'] = int(self.entries['autopilot']['Sun Bright Threshold'].get()) | |
self.ed_ap.config['NavAlignTries'] = int(self.entries['autopilot']['Nav Align Tries'].get()) | |
self.ed_ap.config['JumpTries'] = int(self.entries['autopilot']['Jump Tries'].get()) | |
self.ed_ap.config['DockingRetries'] = int(self.entries['autopilot']['Docking Retries'].get()) | |
self.ed_ap.config['WaitForAutoDockTimer'] = int(self.entries['autopilot']['Wait For Autodock'].get()) | |
self.ed_ap.config['RefuelThreshold'] = int(self.entries['refuel']['Refuel Threshold'].get()) | |
self.ed_ap.config['FuelScoopTimeOut'] = int(self.entries['refuel']['Scoop Timeout'].get()) | |
self.ed_ap.config['FuelThreasholdAbortAP'] = int(self.entries['refuel']['Fuel Threshold Abort'].get()) | |
self.ed_ap.config['OverlayTextXOffset'] = int(self.entries['overlay']['X Offset'].get()) | |
self.ed_ap.config['OverlayTextYOffset'] = int(self.entries['overlay']['Y Offset'].get()) | |
self.ed_ap.config['OverlayTextFontSize'] = int(self.entries['overlay']['Font Size'].get()) | |
self.ed_ap.config['HotKey_StartFSD'] = str(self.entries['buttons']['Start FSD'].get()) | |
self.ed_ap.config['HotKey_StartSC'] = str(self.entries['buttons']['Start SC'].get()) | |
self.ed_ap.config['HotKey_StartRobigo'] = str(self.entries['buttons']['Start Robigo'].get()) | |
self.ed_ap.config['HotKey_StopAllAssists'] = str(self.entries['buttons']['Stop All'].get()) | |
self.ed_ap.config['VoiceEnable'] = self.checkboxvar['Enable Voice'].get() | |
self.ed_ap.config['TCEDestinationFilepath'] = str(self.TCE_Destination_Filepath.get()) | |
except: | |
messagebox.showinfo("Exception", "Invalid float entered") | |
# ckbox.state:(ACTIVE | DISABLED) | |
# ('FSD Route Assist', 'Supercruise Assist', 'Enable Voice', 'Enable CV View') | |
def check_cb(self, field): | |
# print("got event:", checkboxvar['FSD Route Assist'].get(), " ", str(FSD_A_running)) | |
if field == 'FSD Route Assist': | |
if self.checkboxvar['FSD Route Assist'].get() == 1 and self.FSD_A_running == False: | |
self.lab_ck['AFK Combat Assist'].config(state='disabled') | |
self.lab_ck['Supercruise Assist'].config(state='disabled') | |
self.lab_ck['Waypoint Assist'].config(state='disabled') | |
self.lab_ck['Robigo Assist'].config(state='disabled') | |
self.lab_ck['DSS Assist'].config(state='disabled') | |
self.start_fsd() | |
elif self.checkboxvar['FSD Route Assist'].get() == 0 and self.FSD_A_running == True: | |
self.stop_fsd() | |
self.lab_ck['Supercruise Assist'].config(state='active') | |
self.lab_ck['AFK Combat Assist'].config(state='active') | |
self.lab_ck['Waypoint Assist'].config(state='active') | |
self.lab_ck['Robigo Assist'].config(state='active') | |
self.lab_ck['DSS Assist'].config(state='active') | |
if field == 'Supercruise Assist': | |
if self.checkboxvar['Supercruise Assist'].get() == 1 and self.SC_A_running == False: | |
self.lab_ck['FSD Route Assist'].config(state='disabled') | |
self.lab_ck['AFK Combat Assist'].config(state='disabled') | |
self.lab_ck['Waypoint Assist'].config(state='disabled') | |
self.lab_ck['Robigo Assist'].config(state='disabled') | |
self.lab_ck['DSS Assist'].config(state='disabled') | |
self.start_sc() | |
elif self.checkboxvar['Supercruise Assist'].get() == 0 and self.SC_A_running == True: | |
self.stop_sc() | |
self.lab_ck['FSD Route Assist'].config(state='active') | |
self.lab_ck['AFK Combat Assist'].config(state='active') | |
self.lab_ck['Waypoint Assist'].config(state='active') | |
self.lab_ck['Robigo Assist'].config(state='active') | |
self.lab_ck['DSS Assist'].config(state='active') | |
if field == 'Waypoint Assist': | |
if self.checkboxvar['Waypoint Assist'].get() == 1 and self.WP_A_running == False: | |
self.lab_ck['FSD Route Assist'].config(state='disabled') | |
self.lab_ck['Supercruise Assist'].config(state='disabled') | |
self.lab_ck['AFK Combat Assist'].config(state='disabled') | |
self.lab_ck['Robigo Assist'].config(state='disabled') | |
self.lab_ck['DSS Assist'].config(state='disabled') | |
self.start_waypoint() | |
elif self.checkboxvar['Waypoint Assist'].get() == 0 and self.WP_A_running == True: | |
self.stop_waypoint() | |
self.lab_ck['FSD Route Assist'].config(state='active') | |
self.lab_ck['Supercruise Assist'].config(state='active') | |
self.lab_ck['AFK Combat Assist'].config(state='active') | |
self.lab_ck['Robigo Assist'].config(state='active') | |
self.lab_ck['DSS Assist'].config(state='active') | |
if field == 'Robigo Assist': | |
if self.checkboxvar['Robigo Assist'].get() == 1 and self.RO_A_running == False: | |
self.lab_ck['FSD Route Assist'].config(state='disabled') | |
self.lab_ck['Supercruise Assist'].config(state='disabled') | |
self.lab_ck['AFK Combat Assist'].config(state='disabled') | |
self.lab_ck['Waypoint Assist'].config(state='disabled') | |
self.lab_ck['DSS Assist'].config(state='disabled') | |
self.start_robigo() | |
elif self.checkboxvar['Robigo Assist'].get() == 0 and self.RO_A_running == True: | |
self.stop_robigo() | |
self.lab_ck['FSD Route Assist'].config(state='active') | |
self.lab_ck['Supercruise Assist'].config(state='active') | |
self.lab_ck['AFK Combat Assist'].config(state='active') | |
self.lab_ck['Waypoint Assist'].config(state='active') | |
self.lab_ck['DSS Assist'].config(state='active') | |
if field == 'AFK Combat Assist': | |
if self.checkboxvar['AFK Combat Assist'].get() == 1: | |
self.ed_ap.set_afk_combat_assist(True) | |
self.log_msg("AFK Combat Assist start") | |
self.lab_ck['FSD Route Assist'].config(state='disabled') | |
self.lab_ck['Supercruise Assist'].config(state='disabled') | |
self.lab_ck['Waypoint Assist'].config(state='disabled') | |
self.lab_ck['Robigo Assist'].config(state='disabled') | |
self.lab_ck['DSS Assist'].config(state='disabled') | |
elif self.checkboxvar['AFK Combat Assist'].get() == 0: | |
self.ed_ap.set_afk_combat_assist(False) | |
self.log_msg("AFK Combat Assist stop") | |
self.lab_ck['FSD Route Assist'].config(state='active') | |
self.lab_ck['Supercruise Assist'].config(state='active') | |
self.lab_ck['Waypoint Assist'].config(state='active') | |
self.lab_ck['Robigo Assist'].config(state='active') | |
self.lab_ck['DSS Assist'].config(state='active') | |
if field == 'DSS Assist': | |
if self.checkboxvar['DSS Assist'].get() == 1: | |
self.lab_ck['FSD Route Assist'].config(state='disabled') | |
self.lab_ck['AFK Combat Assist'].config(state='disabled') | |
self.lab_ck['Supercruise Assist'].config(state='disabled') | |
self.lab_ck['Waypoint Assist'].config(state='disabled') | |
self.lab_ck['Robigo Assist'].config(state='disabled') | |
self.start_dss() | |
elif self.checkboxvar['DSS Assist'].get() == 0: | |
self.stop_dss() | |
self.lab_ck['FSD Route Assist'].config(state='active') | |
self.lab_ck['Supercruise Assist'].config(state='active') | |
self.lab_ck['AFK Combat Assist'].config(state='active') | |
self.lab_ck['Waypoint Assist'].config(state='active') | |
self.lab_ck['Robigo Assist'].config(state='active') | |
if self.checkboxvar['Enable Randomness'].get(): | |
self.ed_ap.set_randomness(True) | |
else: | |
self.ed_ap.set_randomness(False) | |
if self.checkboxvar['Activate Elite for each key'].get(): | |
self.ed_ap.set_activate_elite_eachkey(True) | |
self.ed_ap.keys.activate_window=True | |
else: | |
self.ed_ap.set_activate_elite_eachkey(False) | |
self.ed_ap.keys.activate_window = False | |
if self.checkboxvar['Automatic logout'].get(): | |
self.ed_ap.set_automatic_logout(True) | |
else: | |
self.ed_ap.set_automatic_logout(False) | |
if self.checkboxvar['Enable Overlay'].get(): | |
self.ed_ap.set_overlay(True) | |
else: | |
self.ed_ap.set_overlay(False) | |
if self.checkboxvar['Enable Voice'].get(): | |
self.ed_ap.set_voice(True) | |
else: | |
self.ed_ap.set_voice(False) | |
if self.checkboxvar['ELW Scanner'].get(): | |
self.ed_ap.set_fss_scan(True) | |
else: | |
self.ed_ap.set_fss_scan(False) | |
if self.checkboxvar['Enable CV View'].get() == 1: | |
self.cv_view = True | |
x = self.root.winfo_x() + self.root.winfo_width() + 4 | |
y = self.root.winfo_y() | |
self.ed_ap.set_cv_view(True, x, y) | |
else: | |
self.cv_view = False | |
self.ed_ap.set_cv_view(False) | |
self.ed_ap.config['DSSButton'] = self.radiobuttonvar['dss_button'].get() | |
if self.radiobuttonvar['debug_mode'].get() == "Error": | |
self.ed_ap.set_log_error(True) | |
elif self.radiobuttonvar['debug_mode'].get() == "Debug": | |
self.ed_ap.set_log_debug(True) | |
elif self.radiobuttonvar['debug_mode'].get() == "Info": | |
self.ed_ap.set_log_info(True) | |
if field == 'Single Waypoint Assist': | |
if self.checkboxvar['Single Waypoint Assist'].get() == 1 and self.SWP_A_running == False: | |
self.start_single_waypoint_assist() | |
elif self.checkboxvar['Single Waypoint Assist'].get() == 0 and self.SWP_A_running == True: | |
self.stop_single_waypoint_assist() | |
def makeform(self, win, ftype, fields, r=0, inc=1, rfrom=0, rto=1000): | |
entries = {} | |
for field in fields: | |
row = tk.Frame(win) | |
row.grid(row=r, column=0, padx=2, pady=2, sticky=(N, S, E, W)) | |
r += 1 | |
if ftype == FORM_TYPE_CHECKBOX: | |
self.checkboxvar[field] = IntVar() | |
lab = Checkbutton(row, text=field, anchor='w', width=27, justify=LEFT, variable=self.checkboxvar[field], command=(lambda field=field: self.check_cb(field))) | |
self.lab_ck[field] = lab | |
else: | |
lab = tk.Label(row, anchor='w', width=20, pady=3, text=field + ": ") | |
if ftype == FORM_TYPE_SPINBOX: | |
ent = tk.Spinbox(row, width=10, from_=rfrom, to=rto, increment=inc) | |
else: | |
ent = tk.Entry(row, width=10) | |
ent.bind('<FocusOut>', self.entry_update) | |
ent.insert(0, "0") | |
lab.grid(row=0, column=0) | |
lab = Hovertip(row, self.tooltips[field], hover_delay=1000) | |
if ftype != FORM_TYPE_CHECKBOX: | |
ent.grid(row=0, column=1) | |
entries[field] = ent | |
return entries | |
def gui_gen(self, win): | |
modes_check_fields = ('FSD Route Assist', 'Supercruise Assist', 'Waypoint Assist', 'Robigo Assist', 'AFK Combat Assist', 'DSS Assist') | |
ship_entry_fields = ('RollRate', 'PitchRate', 'YawRate') | |
autopilot_entry_fields = ('Sun Bright Threshold', 'Nav Align Tries', 'Jump Tries', 'Docking Retries', 'Wait For Autodock') | |
buttons_entry_fields = ('Start FSD', 'Start SC', 'Start Robigo', 'Stop All') | |
refuel_entry_fields = ('Refuel Threshold', 'Scoop Timeout', 'Fuel Threshold Abort') | |
overlay_entry_fields = ('X Offset', 'Y Offset', 'Font Size') | |
# | |
# Define all the menus | |
# | |
menubar = Menu(win, background='#ff8000', foreground='black', activebackground='white', activeforeground='black') | |
file = Menu(menubar, tearoff=0) | |
file.add_command(label="Calibrate Target", command=self.calibrate_callback) | |
file.add_command(label="Calibrate Compass", command=self.calibrate_compass_callback) | |
self.checkboxvar['Enable CV View'] = IntVar() | |
self.checkboxvar['Enable CV View'].set(int(self.ed_ap.config['Enable_CV_View'])) # set IntVar value to the one from config | |
file.add_checkbutton(label='Enable CV View', onvalue=1, offvalue=0, variable=self.checkboxvar['Enable CV View'], command=(lambda field='Enable CV View': self.check_cb(field))) | |
file.add_separator() | |
file.add_command(label="Restart", command=self.restart_program) | |
file.add_command(label="Exit", command=self.close_window) # win.quit) | |
menubar.add_cascade(label="File", menu=file) | |
help = Menu(menubar, tearoff=0) | |
help.add_command(label="Check for Updates", command=self.check_updates) | |
help.add_command(label="View Changelog", command=self.open_changelog) | |
help.add_separator() | |
help.add_command(label="Join Discord", command=self.open_discord) | |
help.add_command(label="About", command=self.about) | |
menubar.add_cascade(label="Help", menu=help) | |
win.config(menu=menubar) | |
# notebook pages | |
nb = ttk.Notebook(win) | |
nb.grid() | |
page0 = Frame(nb) | |
page1 = Frame(nb) | |
page2 = Frame(nb) | |
nb.add(page0, text="Main") # main page | |
nb.add(page1, text="Settings") # options page | |
nb.add(page2, text="Debug/Test") # debug/test page | |
# main options block | |
blk_main = tk.Frame(page0) | |
blk_main.grid(row=0, column=0, padx=10, pady=5, sticky=(E, W)) | |
blk_main.columnconfigure([0, 1], weight=1, minsize=100, uniform="group1") | |
# ap mode checkboxes block | |
blk_modes = LabelFrame(blk_main, text="MODE") | |
blk_modes.grid(row=0, column=0, padx=2, pady=2, sticky=(N, S, E, W)) | |
self.makeform(blk_modes, FORM_TYPE_CHECKBOX, modes_check_fields) | |
# ship values block | |
blk_ship = LabelFrame(blk_main, text="SHIP") | |
blk_ship.grid(row=0, column=1, padx=2, pady=2, sticky=(N, S, E, W)) | |
self.entries['ship'] = self.makeform(blk_ship, FORM_TYPE_SPINBOX, ship_entry_fields, 0, 0.5) | |
lbl_sun_pitch_up = tk.Label(blk_ship, text='SunPitchUp +/- Time:', anchor='w', width=20) | |
lbl_sun_pitch_up.grid(row=3, column=0, pady=3, sticky=(N, S, E, W)) | |
spn_sun_pitch_up = tk.Spinbox(blk_ship, width=10, from_=-100, to=100, increment=0.5) | |
spn_sun_pitch_up.grid(row=3, column=1, padx=2, pady=2, sticky=(N, S, E, W)) | |
spn_sun_pitch_up.bind('<FocusOut>', self.entry_update) | |
self.entries['ship']['SunPitchUp+Time'] = spn_sun_pitch_up | |
btn_tst_roll = Button(blk_ship, text='Test Roll', command=self.ship_tst_roll) | |
btn_tst_roll.grid(row=4, column=0, padx=2, pady=2, columnspan=2, sticky=(N, E, W, S)) | |
btn_tst_pitch = Button(blk_ship, text='Test Pitch', command=self.ship_tst_pitch) | |
btn_tst_pitch.grid(row=5, column=0, padx=2, pady=2, columnspan=2, sticky=(N, E, W, S)) | |
btn_tst_yaw = Button(blk_ship, text='Test Yaw', command=self.ship_tst_yaw) | |
btn_tst_yaw.grid(row=6, column=0, padx=2, pady=2, columnspan=2, sticky=(N, E, W, S)) | |
# profile load / info button in ship values block | |
self.ship_filelabel = StringVar() | |
self.ship_filelabel.set("<no config loaded>") | |
btn_ship_file = Button(blk_ship, textvariable=self.ship_filelabel, command=self.open_ship_file) | |
btn_ship_file.grid(row=8, column=0, padx=2, pady=2, sticky=(N, E, W)) | |
tip_ship_file = Hovertip(btn_ship_file, self.tooltips['Ship Config Button'], hover_delay=1000) | |
# waypoints button block | |
blk_wp_buttons = tk.LabelFrame(page0, text="Waypoints") | |
blk_wp_buttons.grid(row=1, column=0, padx=10, pady=5, columnspan=2, sticky=(N, S, E, W)) | |
blk_wp_buttons.columnconfigure([0, 1], weight=1, minsize=100, uniform="group1") | |
self.wp_filelabel = StringVar() | |
self.wp_filelabel.set("<no list loaded>") | |
btn_wp_file = Button(blk_wp_buttons, textvariable=self.wp_filelabel, command=self.open_wp_file) | |
btn_wp_file.grid(row=0, column=0, padx=2, pady=2, columnspan=2, sticky=(N, E, W, S)) | |
tip_wp_file = Hovertip(btn_wp_file, self.tooltips['Waypoint List Button'], hover_delay=1000) | |
btn_reset = Button(blk_wp_buttons, text='Reset List', command=self.reset_wp_file) | |
btn_reset.grid(row=1, column=0, padx=2, pady=2, columnspan=2, sticky=(N, E, W, S)) | |
tip_reset = Hovertip(btn_reset, self.tooltips['Reset Waypoint List'], hover_delay=1000) | |
# log window | |
log = LabelFrame(page0, text="LOG") | |
log.grid(row=3, column=0, padx=12, pady=5, sticky=(N, S, E, W)) | |
scrollbar = Scrollbar(log) | |
scrollbar.grid(row=0, column=1, sticky=(N, S)) | |
mylist = Listbox(log, width=72, height=10, yscrollcommand=scrollbar.set) | |
mylist.grid(row=0, column=0) | |
scrollbar.config(command=mylist.yview) | |
# settings block | |
blk_settings = tk.Frame(page1) | |
blk_settings.grid(row=0, column=0, padx=10, pady=5, sticky=(E, W)) | |
blk_main.columnconfigure([0, 1], weight=1, minsize=100, uniform="group1") | |
# autopilot settings block | |
blk_ap = LabelFrame(blk_settings, text="AUTOPILOT") | |
blk_ap.grid(row=0, column=0, padx=2, pady=2, sticky=(N, S, E, W)) | |
self.entries['autopilot'] = self.makeform(blk_ap, FORM_TYPE_SPINBOX, autopilot_entry_fields) | |
self.checkboxvar['Enable Randomness'] = BooleanVar() | |
cb_random = Checkbutton(blk_ap, text='Enable Randomness', anchor='w', pady=3, justify=LEFT, onvalue=1, offvalue=0, variable=self.checkboxvar['Enable Randomness'], command=(lambda field='Enable Randomness': self.check_cb(field))) | |
cb_random.grid(row=5, column=0, columnspan=2, sticky=(W)) | |
self.checkboxvar['Activate Elite for each key'] = BooleanVar() | |
cb_activate_elite = Checkbutton(blk_ap, text='Activate Elite for each key', anchor='w', pady=3, justify=LEFT, onvalue=1, offvalue=0, variable=self.checkboxvar['Activate Elite for each key'], command=(lambda field='Activate Elite for each key': self.check_cb(field))) | |
cb_activate_elite.grid(row=6, column=0, columnspan=2, sticky=(W)) | |
self.checkboxvar['Automatic logout'] = BooleanVar() | |
cb_logout = Checkbutton(blk_ap, text='Automatic logout', anchor='w', pady=3, justify=LEFT, onvalue=1, offvalue=0, variable=self.checkboxvar['Automatic logout'], command=(lambda field='Automatic logout': self.check_cb(field))) | |
cb_logout.grid(row=7, column=0, columnspan=2, sticky=(W)) | |
# buttons settings block | |
blk_buttons = LabelFrame(blk_settings, text="BUTTONS") | |
blk_buttons.grid(row=0, column=1, padx=2, pady=2, sticky=(N, S, E, W)) | |
blk_dss = Frame(blk_buttons) | |
blk_dss.grid(row=0, column=0, columnspan=2, padx=0, pady=0, sticky=(N, S, E, W)) | |
lb_dss = Label(blk_dss, width=18, anchor='w', pady=3, text="DSS Button: ") | |
lb_dss.grid(row=0, column=0, sticky=(W)) | |
self.radiobuttonvar['dss_button'] = StringVar() | |
rb_dss_primary = Radiobutton(blk_dss, pady=3, text="Primary", variable=self.radiobuttonvar['dss_button'], value="Primary", command=(lambda field='dss_button': self.check_cb(field))) | |
rb_dss_primary.grid(row=0, column=1, sticky=(W)) | |
rb_dss_secandary = Radiobutton(blk_dss, pady=3, text="Secondary", variable=self.radiobuttonvar['dss_button'], value="Secondary", command=(lambda field='dss_button': self.check_cb(field))) | |
rb_dss_secandary.grid(row=1, column=1, sticky=(W)) | |
self.entries['buttons'] = self.makeform(blk_buttons, FORM_TYPE_ENTRY, buttons_entry_fields, 2) | |
# refuel settings block | |
blk_fuel = LabelFrame(blk_settings, text="FUEL") | |
blk_fuel.grid(row=1, column=0, padx=2, pady=2, sticky=(N, S, E, W)) | |
self.entries['refuel'] = self.makeform(blk_fuel, FORM_TYPE_SPINBOX, refuel_entry_fields) | |
# overlay settings block | |
blk_overlay = LabelFrame(blk_settings, text="OVERLAY") | |
blk_overlay.grid(row=1, column=1, padx=2, pady=2, sticky=(N, S, E, W)) | |
self.checkboxvar['Enable Overlay'] = BooleanVar() | |
cb_enable = Checkbutton(blk_overlay, text='Enable (requires restart)', onvalue=1, offvalue=0, anchor='w', pady=3, justify=LEFT, variable=self.checkboxvar['Enable Overlay'], command=(lambda field='Enable Overlay': self.check_cb(field))) | |
cb_enable.grid(row=0, column=0, columnspan=2, sticky=(W)) | |
self.entries['overlay'] = self.makeform(blk_overlay, FORM_TYPE_SPINBOX, overlay_entry_fields, 1, 1.0, 0.0, 3000.0) | |
# tts / voice settings block | |
blk_voice = LabelFrame(blk_settings, text="VOICE") | |
blk_voice.grid(row=2, column=0, padx=2, pady=2, sticky=(N, S, E, W)) | |
self.checkboxvar['Enable Voice'] = BooleanVar() | |
cb_enable = Checkbutton(blk_voice, text='Enable', onvalue=1, offvalue=0, anchor='w', pady=3, justify=LEFT, variable=self.checkboxvar['Enable Voice'], command=(lambda field='Enable Voice': self.check_cb(field))) | |
cb_enable.grid(row=0, column=0, columnspan=2, sticky=(W)) | |
# Scanner settings block | |
blk_voice = LabelFrame(blk_settings, text="ELW SCANNER") | |
blk_voice.grid(row=2, column=1, padx=2, pady=2, sticky=(N, S, E, W)) | |
self.checkboxvar['ELW Scanner'] = BooleanVar() | |
cb_enable = Checkbutton(blk_voice, text='Enable', onvalue=1, offvalue=0, anchor='w', pady=3, justify=LEFT, variable=self.checkboxvar['ELW Scanner'], command=(lambda field='ELW Scanner': self.check_cb(field))) | |
cb_enable.grid(row=0, column=0, columnspan=2, sticky=(W)) | |
# settings button block | |
blk_settings_buttons = tk.Frame(page1) | |
blk_settings_buttons.grid(row=3, column=0, padx=10, pady=5, sticky=(N, S, E, W)) | |
blk_settings_buttons.columnconfigure([0, 1], weight=1, minsize=100) | |
btn_save = Button(blk_settings_buttons, text='Save All Settings', command=self.save_settings) | |
btn_save.grid(row=0, column=0, padx=2, pady=2, columnspan=2, sticky=(N, E, W, S)) | |
# debug block | |
blk_debug = tk.Frame(page2) | |
blk_debug.grid(row=0, column=0, padx=10, pady=5, sticky=(E, W)) | |
blk_debug.columnconfigure([0, 1], weight=1, minsize=100, uniform="group2") | |
# debug settings block | |
blk_debug_settings = LabelFrame(blk_debug, text="DEBUG") | |
blk_debug_settings.grid(row=0, column=0, padx=2, pady=2, sticky=(N, S, E, W)) | |
self.radiobuttonvar['debug_mode'] = StringVar() | |
rb_debug_debug = Radiobutton(blk_debug_settings, pady=3, text="Debug + Info + Errors", variable=self.radiobuttonvar['debug_mode'], value="Debug", command=(lambda field='debug_mode': self.check_cb(field))) | |
rb_debug_debug.grid(row=0, column=1, columnspan=2, sticky=(W)) | |
rb_debug_info = Radiobutton(blk_debug_settings, pady=3, text="Info + Errors", variable=self.radiobuttonvar['debug_mode'], value="Info", command=(lambda field='debug_mode': self.check_cb(field))) | |
rb_debug_info.grid(row=1, column=1, columnspan=2, sticky=(W)) | |
rb_debug_error = Radiobutton(blk_debug_settings, pady=3, text="Errors only (default)", variable=self.radiobuttonvar['debug_mode'], value="Error", command=(lambda field='debug_mode': self.check_cb(field))) | |
rb_debug_error.grid(row=2, column=1, columnspan=2, sticky=(W)) | |
btn_open_logfile = Button(blk_debug_settings, text='Open Log File', command=self.open_logfile) | |
btn_open_logfile.grid(row=3, column=0, padx=2, pady=2, columnspan=2, sticky=(N, E, W, S)) | |
# debug settings block | |
blk_single_waypoint_asst = LabelFrame(page2, text="Single Waypoint Assist") | |
blk_single_waypoint_asst.grid(row=1, column=0, padx=10, pady=5, columnspan=2, sticky=(N, S, E, W)) | |
blk_single_waypoint_asst.columnconfigure(0, weight=1, minsize=10, uniform="group1") | |
blk_single_waypoint_asst.columnconfigure(1, weight=3, minsize=10, uniform="group1") | |
lbl_system = tk.Label(blk_single_waypoint_asst, text='System:') | |
lbl_system.grid(row=0, column=0, padx=2, pady=2, columnspan=1, sticky=(N, E, W, S)) | |
txt_system = Entry(blk_single_waypoint_asst, textvariable=self.single_waypoint_system) | |
txt_system.grid(row=0, column=1, padx=2, pady=2, columnspan=1, sticky=(N, E, W, S)) | |
lbl_station = tk.Label(blk_single_waypoint_asst, text='Station:') | |
lbl_station.grid(row=1, column=0, padx=2, pady=2, columnspan=1, sticky=(N, E, W, S)) | |
txt_station = Entry(blk_single_waypoint_asst, textvariable=self.single_waypoint_station) | |
txt_station.grid(row=1, column=1, padx=2, pady=2, columnspan=1, sticky=(N, E, W, S)) | |
self.checkboxvar['Single Waypoint Assist'] = BooleanVar() | |
cb_single_waypoint = Checkbutton(blk_single_waypoint_asst, text='Single Waypoint Assist', onvalue=1, offvalue=0, anchor='w', pady=3, justify=LEFT, variable=self.checkboxvar['Single Waypoint Assist'], command=(lambda field='Single Waypoint Assist': self.check_cb(field))) | |
cb_single_waypoint.grid(row=2, column=0, padx=2, pady=2, columnspan=2, sticky=(N, E, W, S)) | |
lbl_tce = tk.Label(blk_single_waypoint_asst, text='Trade Computer Extension (TCE)', fg="blue", cursor="hand2") | |
lbl_tce.bind("<Button-1>", lambda e: hyperlink_callback("https://forums.frontier.co.uk/threads/trade-computer-extension-mk-ii.223056/")) | |
lbl_tce.grid(row=3, column=0, padx=2, pady=2, columnspan=2, sticky=(N, E, W, S)) | |
lbl_tce_dest = tk.Label(blk_single_waypoint_asst, text='TCE Dest json:') | |
lbl_tce_dest.grid(row=4, column=0, padx=2, pady=2, columnspan=1, sticky=(N, E, W, S)) | |
txt_tce_dest = Entry(blk_single_waypoint_asst, textvariable=self.TCE_Destination_Filepath) | |
txt_tce_dest.bind('<FocusOut>', self.entry_update) | |
txt_tce_dest.grid(row=4, column=1, padx=2, pady=2, columnspan=1, sticky=(N, E, W, S)) | |
btn_load_tce = Button(blk_single_waypoint_asst, text='Load TCE Destination', command=self.load_tce_dest) | |
btn_load_tce.grid(row=5, column=0, padx=2, pady=2, columnspan=2, sticky=(N, E, W, S)) | |
blk_debug_buttons = tk.Frame(page2) | |
blk_debug_buttons.grid(row=3, column=0, padx=10, pady=5, columnspan=2, sticky=(N, S, E, W)) | |
blk_debug_buttons.columnconfigure([0, 1], weight=1, minsize=100) | |
btn_save = Button(blk_debug_buttons, text='Save All Settings', command=self.save_settings) | |
btn_save.grid(row=6, column=0, padx=2, pady=2, columnspan=2, sticky=(N, E, W, S)) | |
# Statusbar | |
statusbar = Frame(win) | |
statusbar.grid(row=4, column=0) | |
self.status = tk.Label(win, text="Status: ", bd=1, relief=tk.SUNKEN, anchor=tk.W, justify=LEFT, width=29) | |
self.jumpcount = tk.Label(statusbar, text="<info> ", bd=1, relief=tk.SUNKEN, anchor=tk.W, justify=LEFT, width=40) | |
self.status.pack(in_=statusbar, side=LEFT, fill=BOTH, expand=True) | |
self.jumpcount.pack(in_=statusbar, side=RIGHT, fill=Y, expand=False) | |
return mylist | |
def restart_program(self): | |
logger.debug("Entered: restart_program") | |
print("restart now") | |
self.stop_fsd() | |
self.stop_sc() | |
self.ed_ap.quit() | |
sleep(0.1) | |
import sys | |
print("argv was", sys.argv) | |
print("sys.executable was", sys.executable) | |
print("restart now") | |
import os | |
os.execv(sys.executable, ['python'] + sys.argv) | |
def main(): | |
# handle = win32gui.FindWindow(0, "Elite - Dangerous (CLIENT)") | |
# if handle != None: | |
# win32gui.SetForegroundWindow(handle) # put the window in foreground | |
root = tk.Tk() | |
app = APGui(root) | |
root.mainloop() | |
if __name__ == "__main__": | |
main() | |
--- | |
File: /EDGalaxyMap.py | |
--- | |
from __future__ import annotations | |
from EDAP_data import GuiFocusGalaxyMap | |
from OCR import OCR | |
from StatusParser import StatusParser | |
from time import sleep | |
from EDlogger import logger | |
from pyautogui import typewrite | |
class EDGalaxyMap: | |
""" Handles the Galaxy Map. """ | |
def __init__(self, ed_ap, screen, keys, cb, is_odyssey=True): | |
self.ap = ed_ap | |
self.is_odyssey = is_odyssey | |
self.screen = screen | |
self.ocr = OCR(screen) | |
self.keys = keys | |
self.status_parser = StatusParser() | |
self.ap_ckb = cb | |
def set_gal_map_dest_bookmark(self, ap, bookmark_type: str, bookmark_position: int) -> bool: | |
""" Set the gal map destination using a bookmark. | |
@param ap: ED_AP reference. | |
@param bookmark_type: The bookmark type (Favorite, System, Body, Station or Settlement), Favorite | |
being the default if no match is made with the other options. | |
@param bookmark_position: The position in the bookmark list, starting at 1 for the first bookmark. | |
@return: True if bookmark could be selected, else False | |
""" | |
if self.is_odyssey and bookmark_position > 0: | |
self.goto_galaxy_map() | |
ap.keys.send('UI_Left') # Go to BOOKMARKS | |
sleep(.5) | |
ap.keys.send('UI_Select') # Select BOOKMARKS | |
sleep(.25) | |
ap.keys.send('UI_Right') # Go to FAVORITES | |
sleep(.25) | |
# If bookmark type is Fav, do nothing as this is the first item | |
if bookmark_type.lower().startswith("sys"): | |
ap.keys.send('UI_Down') # Go to SYSTEMS | |
elif bookmark_type.lower().startswith("bod"): | |
ap.keys.send('UI_Down', repeat=2) # Go to BODIES | |
elif bookmark_type.lower().startswith("sta"): | |
ap.keys.send('UI_Down', repeat=3) # Go to STATIONS | |
elif bookmark_type.lower().startswith("set"): | |
ap.keys.send('UI_Down', repeat=4) # Go to SETTLEMENTS | |
sleep(.25) | |
ap.keys.send('UI_Select') # Select bookmark type, moves you to bookmark list | |
sleep(.25) | |
ap.keys.send('UI_Down', repeat=bookmark_position - 1) | |
sleep(.25) | |
ap.keys.send('UI_Select', hold=3.0) | |
# Close Galaxy map | |
ap.keys.send('GalaxyMapOpen') | |
sleep(0.5) | |
return True | |
return False | |
def set_gal_map_destination_text(self, ap, target_name, target_select_cb=None) -> bool: | |
""" Call either the Odyssey or Horizons version of the Galactic Map sequence. """ | |
if not self.is_odyssey: | |
return ap.galaxy_map.set_gal_map_destination_text_horizons(ap, target_name, target_select_cb) | |
else: | |
return ap.galaxy_map.set_gal_map_destination_text_odyssey(ap, target_name) | |
def set_gal_map_destination_text_horizons(self, ap, target_name, target_select_cb=None) -> bool: | |
""" This sequence for the Horizons. """ | |
self.goto_galaxy_map() | |
ap.keys.send('CycleNextPanel') | |
sleep(1) | |
ap.keys.send('UI_Select') | |
sleep(2) | |
typewrite(target_name, interval=0.25) | |
sleep(1) | |
# send enter key | |
ap.keys.send_key('Down', 28) | |
sleep(0.05) | |
ap.keys.send_key('Up', 28) | |
sleep(7) | |
ap.keys.send('UI_Right') | |
sleep(1) | |
ap.keys.send('UI_Select') | |
# if got passed through the ship() object, lets call it to see if a target has been | |
# selected yet... otherwise we wait. If long route, it may take a few seconds | |
if target_select_cb is not None: | |
while not target_select_cb()['target']: | |
sleep(1) | |
# Close Galaxy map | |
ap.keys.send('GalaxyMapOpen') | |
sleep(2) | |
return True | |
def set_gal_map_destination_text_odyssey(self, ap, target_name) -> bool: | |
""" This sequence for the Odyssey. """ | |
self.goto_galaxy_map() | |
# Check if the current nav route is to the target system | |
last_nav_route_sys = ap.nav_route.get_last_system() | |
if last_nav_route_sys.upper() == target_name.upper(): | |
# Close Galaxy map | |
ap.keys.send('GalaxyMapOpen') | |
return True | |
# navigate to and select: search field | |
ap.keys.send('UI_Up') | |
sleep(0.05) | |
ap.keys.send('UI_Select') | |
sleep(0.05) | |
# type in the System name | |
typewrite(target_name, interval=0.25) | |
logger.debug(f"Entered system name: {target_name}") | |
sleep(0.05) | |
# send enter key (removes focus out of input field) | |
ap.keys.send_key('Down', 28) # 28=ENTER | |
sleep(0.05) | |
ap.keys.send_key('Up', 28) # 28=ENTER | |
sleep(0.05) | |
# According to some reports, the ENTER key does not always reselect the text | |
# box, so this down and up will reselect the text box. | |
ap.keys.send('UI_Down') | |
sleep(0.05) | |
ap.keys.send('UI_Up') | |
sleep(0.05) | |
# navigate to and select: search button | |
ap.keys.send('UI_Right') # to >| button | |
sleep(0.05) | |
correct_route = False | |
while not correct_route: | |
# Store the current nav route system | |
last_nav_route_sys = ap.nav_route.get_last_system() | |
logger.debug(f"Previous Nav Route dest: {last_nav_route_sys}.") | |
# Select first (or next) system | |
ap.keys.send('UI_Select') # Select >| button | |
sleep(0.05) | |
# zoom camera which puts focus back on the map | |
ap.keys.send('CamZoomIn') | |
sleep(0.05) | |
# plot route. Not that once the system has been selected, as shown in the info panel | |
# and the gal map has focus, there is no need to wait for the map to bring the system | |
# to the center screen, the system can be selected while the map is moving. | |
ap.keys.send('UI_Select', hold=0.75) | |
sleep(0.05) | |
# if got passed through the ship() object, lets call it to see if a target has been | |
# selected yet... otherwise we wait. If long route, it may take a few seconds | |
if ap.nav_route is not None: | |
logger.debug(f"Waiting for Nav Route to update.") | |
while 1: | |
curr_nav_route_sys = ap.nav_route.get_last_system() | |
# Check if the nav route has been changed (right or wrong) | |
if curr_nav_route_sys.upper() != last_nav_route_sys.upper(): | |
logger.debug(f"Current Nav Route dest: {curr_nav_route_sys}.") | |
# Check if this nav route is correct | |
if curr_nav_route_sys.upper() == target_name.upper(): | |
logger.debug(f"Nav Route updated correctly.") | |
# Break loop and exit | |
correct_route = True | |
break | |
else: | |
# Try the next system, go back to the search bar | |
logger.debug(f"Nav Route updated with wrong target. Select next target.") | |
ap.keys.send('UI_Up') | |
break | |
else: | |
# Cannot check route, so assume right | |
logger.debug(f"Unable to check Nav Route, so assuming it is correct.") | |
correct_route = True | |
# Close Galaxy map | |
ap.keys.send('GalaxyMapOpen') | |
return True | |
def set_next_system(self, ap, target_system) -> bool: | |
""" Sets the next system to jump to, or the final system to jump to. | |
If the system is already selected or is selected correctly, returns True, | |
otherwise False. | |
""" | |
# Call sequence to select route | |
if self.set_gal_map_destination_text(ap, target_system, None): | |
return True | |
else: | |
# Error setting target | |
logger.warning("Error setting waypoint, breaking") | |
return False | |
def goto_galaxy_map(self): | |
"""Open Galaxy Map if we are not there. Waits for map to load. Selects the search bar. | |
""" | |
if self.status_parser.get_gui_focus() != GuiFocusGalaxyMap: | |
logger.debug("Opening Galaxy Map") | |
# Goto cockpit view | |
self.ap.ship_control.goto_cockpit_view() | |
# Goto Galaxy Map | |
self.keys.send('GalaxyMapOpen') | |
# Wait for map to load | |
# TODO - check this to OCR check | |
sleep(2) | |
self.keys.send('UI_Up') # Go up to search bar | |
else: | |
logger.debug("Galaxy Map is already open") | |
self.keys.send('UI_Left', repeat=2) | |
self.keys.send('UI_Up', hold=2) # Go up to search bar. Allows 1 left to bookmarks. | |
--- | |
File: /EDGraphicsSettings.py | |
--- | |
from os.path import isfile | |
from EDlogger import logger | |
import xmltodict | |
from os import environ | |
class EDGraphicsSettings: | |
""" Handles the Graphics DisplaySettings.xml and Settings.xml files. """ | |
def __init__(self, display_file_path=None, settings_file_path=None): | |
self.fullscreen = '' | |
self.fullscreen_str = '' | |
self.screenwidth = '' | |
self.screenheight = '' | |
self.monitor = '' | |
self.fov = '' | |
self.display_settings_filepath = display_file_path if display_file_path else \ | |
(environ[ | |
'LOCALAPPDATA'] + "\\Frontier Developments\\Elite Dangerous\\Options\\Graphics\\DisplaySettings.xml") | |
self.settings_filepath = settings_file_path if settings_file_path else \ | |
(environ['LOCALAPPDATA'] + "\\Frontier Developments\\Elite Dangerous\\Options\\Graphics\\Settings.xml") | |
if not isfile(self.display_settings_filepath): | |
logger.error( | |
f"Elite Dangerous graphics display settings file does not exist: {self.display_settings_filepath}.") | |
raise Exception( | |
f"Elite Dangerous graphics display settings file does not exist: {self.display_settings_filepath}.") | |
if not isfile(self.settings_filepath): | |
logger.error(f"Elite Dangerous settings file does not exist: {self.settings_filepath}.") | |
raise Exception(f"Elite Dangerous settings file does not exist: {self.settings_filepath}.") | |
# Read graphics display settings xml file data | |
logger.info(f"Reading ED graphics display settings from '{self.display_settings_filepath}'.") | |
self.display_settings = self.read_settings(self.display_settings_filepath) | |
# Read graphics display settings xml file data | |
logger.info(f"Reading ED graphics settings from '{self.settings_filepath}'.") | |
self.settings = self.read_settings(self.settings_filepath) | |
# Process graphics display settings | |
if self.display_settings is not None: | |
self.screenwidth = self.display_settings['DisplayConfig']['ScreenWidth'] | |
logger.debug(f"Elite Dangerous Display Config 'ScreenWidth': {self.screenwidth}.") | |
self.screenheight = self.display_settings['DisplayConfig']['ScreenHeight'] | |
logger.debug(f"Elite Dangerous Display Config 'ScreenHeight': {self.screenheight}.") | |
self.fullscreen = self.display_settings['DisplayConfig'][ | |
'FullScreen'] # 0=Windowed, 1=Fullscreen, 2=Borderless | |
options = ["Windowed", "Fullscreen", "Borderless"] | |
self.fullscreen_str = options[int(self.fullscreen)] | |
logger.debug(f"Elite Dangerous Display Config 'Fullscreen': {self.fullscreen_str}.") | |
self.monitor = self.display_settings['DisplayConfig']['Monitor'] | |
logger.debug(f"Elite Dangerous Display Config 'Monitor': {self.monitor}.") | |
if not self.fullscreen_str.upper() == "Borderless".upper(): | |
logger.error("Elite Dangerous is not set to BORDERLESS in graphics display settings.") | |
raise Exception('Elite Dangerous is not set to BORDERLESS in graphics display settings.') | |
# Process graphics settings | |
if self.settings is not None: | |
self.fov = self.settings['GraphicsOptions']['FOV'] | |
logger.debug(f"Elite Dangerous Graphics Options 'FOV': {self.fov}.") | |
@staticmethod | |
def read_settings(filename) -> dict: | |
""" Reads an XML settings file to a Dict and returns the dict. """ | |
try: | |
with open(filename, 'r') as file: | |
my_xml = file.read() | |
my_dict = xmltodict.parse(my_xml) | |
return my_dict | |
except OSError as e: | |
logger.error(f"OS Error reading Elite Dangerous display settings file: {filename}.") | |
raise Exception(f"OS Error reading Elite Dangerous display settings file: {filename}.") | |
def main(): | |
gs = EDGraphicsSettings() | |
if __name__ == "__main__": | |
main() | |
--- | |
File: /EDInternalStatusPanel.py | |
--- | |
from __future__ import annotations | |
import logging | |
from time import sleep | |
import cv2 | |
from EDAP_data import * | |
from EDKeys import EDKeys | |
from OCR import OCR | |
from Screen import Screen | |
from Screen_Regions import size_scale_for_station | |
from StatusParser import StatusParser | |
from EDlogger import logger | |
class EDInternalStatusPanel: | |
""" The Internal (Right hand) Ship Status Panel. """ | |
def __init__(self, ed_ap, screen, keys, cb): | |
self.ap = ed_ap | |
self.locale = self.ap.locale | |
self.screen = screen | |
self.ocr = OCR(screen) | |
self.keys = keys | |
self.status_parser = StatusParser() | |
self.ap_ckb = cb | |
self.modules_tab_text = self.locale["INT_PNL_TAB_MODULES"] | |
self.fire_groups_tab_text = self.locale["INT_PNL_TAB_FIRE_GROUPS"] | |
self.ship_tab_text = self.locale["INT_PNL_TAB_SHIP"] | |
self.inventory_tab_text = self.locale["INT_PNL_TAB_INVENTORY"] | |
self.storage_tab_text = self.locale["INT_PNL_TAB_STORAGE"] | |
self.status_tab_text = self.locale["INT_PNL_TAB_STATUS"] | |
# The rect is top left x, y, and bottom right x, y in fraction of screen resolution | |
self.reg = {'right_panel': {'rect': [0.25, 0.0, 1.0, 0.5]}} | |
self.nav_pnl_tab_width = 140 # Nav panel tab width in pixels at 1920x1080 | |
self.nav_pnl_tab_height = 35 # Nav panel tab height in pixels at 1920x1080 | |
def show_right_panel(self): | |
""" Shows the Internal (Right) Panel. Opens the Internal Panel if not already open. | |
Returns True if successful, else False. | |
""" | |
logger.debug("show_right_panel: entered") | |
# Is nav panel active? | |
active, active_tab_name = self.is_right_panel_active() | |
if active: | |
# Store image | |
image = self.screen.get_screen_full() | |
cv2.imwrite(f'test/internal-panel/int_panel_full.png', image) | |
return active, active_tab_name | |
else: | |
print("Open Internal Panel") | |
logger.debug("show_right_panel: Open Internal Panel") | |
self.ap.ship_control.goto_cockpit_view() | |
self.keys.send("HeadLookReset") | |
self.keys.send('UIFocus', state=1) | |
self.keys.send('UI_Right') | |
self.keys.send('UIFocus', state=0) | |
sleep(0.5) | |
# Check if it opened | |
active, active_tab_name = self.is_right_panel_active() | |
if active: | |
# Store image | |
image = self.screen.get_screen_full() | |
cv2.imwrite(f'test/internal-panel/internal_panel_full.png', image) | |
return active, active_tab_name | |
else: | |
return False, "" | |
def show_inventory_tab(self) -> bool | None: | |
""" Shows the INVENTORY tab of the Nav Panel. Opens the Nav Panel if not already open. | |
Returns True if successful, else False. | |
""" | |
logger.debug("show_inventory_tab: entered") | |
# Show nav panel | |
active, active_tab_name = self.show_right_panel() | |
if active is None: | |
return None | |
if not active: | |
print("Internal (Right) Panel could not be opened") | |
return False | |
elif active_tab_name is self.inventory_tab_text: | |
# Do nothing | |
return True | |
elif active_tab_name is self.modules_tab_text: | |
self.keys.send('CycleNextPanel', repeat=3) | |
return True | |
elif active_tab_name is self.fire_groups_tab_text: | |
self.keys.send('CycleNextPanel', repeat=2) | |
return True | |
elif active_tab_name is self.ship_tab_text: | |
self.keys.send('CycleNextPanel', repeat=1) | |
return True | |
elif active_tab_name is self.storage_tab_text: | |
self.keys.send('CycleNextPanel', repeat=7) | |
return True | |
elif active_tab_name is self.status_tab_text: | |
self.keys.send('CycleNextPanel', repeat=6) | |
return True | |
def is_right_panel_active(self) -> (bool, str): | |
""" Determine if the Nav Panel is open and if so, which tab is active. | |
Returns True if active, False if not and also the string of the tab name. | |
""" | |
logger.debug("is_right_panel_active: entered") | |
# Check if nav panel is open | |
if not self.status_parser.wait_for_gui_focus(GuiFocusInternalPanel, 3): | |
logger.debug("is_right_panel_active: right panel not focused") | |
return False, "" | |
logger.debug("is_right_panel_active: right panel is focused") | |
# Try this 'n' times before giving up | |
for i in range(10): | |
# Is open, so proceed | |
image = self.ocr.capture_region_pct(self.reg['right_panel']) | |
# tab_bar = self.capture_tab_bar() | |
# if tab_bar is None: | |
# return None | |
# Determine the nav panel tab size at this resolution | |
scl_row_w, scl_row_h = size_scale_for_station(self.nav_pnl_tab_width, self.nav_pnl_tab_height, | |
self.screen.screen_width, self.screen.screen_height) | |
img_selected, ocr_data, ocr_textlist = self.ocr.get_highlighted_item_data(image, scl_row_w, scl_row_h) | |
if img_selected is not None: | |
logger.debug("is_right_panel_active: image selected") | |
logger.debug(f"is_right_panel_active: OCR: {ocr_textlist}") | |
if self.modules_tab_text in str(ocr_textlist): | |
return True, self.modules_tab_text | |
if self.fire_groups_tab_text in str(ocr_textlist): | |
return True, self.fire_groups_tab_text | |
if self.ship_tab_text in str(ocr_textlist): | |
return True, self.ship_tab_text | |
if self.inventory_tab_text in str(ocr_textlist): | |
return True, self.inventory_tab_text | |
if self.storage_tab_text in str(ocr_textlist): | |
return True, self.storage_tab_text | |
if self.status_tab_text in str(ocr_textlist): | |
return True, self.status_tab_text | |
else: | |
logger.debug("is_right_panel_active: no image selected") | |
# Wait and retry | |
sleep(1) | |
# In case we are on a picture tab, cycle to the next tab | |
self.keys.send('CycleNextPanel') | |
# Did not find anything | |
return False, "" | |
def hide_right_panel(self): | |
""" Hides the Nav Panel if open. | |
""" | |
# active, active_tab_name = self.is_nav_panel_active() | |
# if active is not None: | |
# Is nav panel active? | |
if self.status_parser.get_gui_focus() == GuiFocusInternalPanel: | |
self.keys.send("UI_Back") | |
self.keys.send("HeadLookReset") | |
def transfer_to_fleetcarrier(self, ap): | |
""" Transfer all goods to Fleet Carrier """ | |
self.ap_ckb('log+vce', "Executing transfer to Fleet Carrier.") | |
logger.debug("transfer_to_fleetcarrier: entered") | |
# Go to the internal (right) panel inventory tab | |
self.show_inventory_tab() | |
# Assumes on the INVENTORY tab | |
ap.keys.send('UI_Right') | |
sleep(0.1) | |
ap.keys.send('UI_Up') # To FILTERS | |
sleep(0.1) | |
ap.keys.send('UI_Right') # To TRANSFER >> | |
sleep(0.1) | |
ap.keys.send('UI_Select') # Click TRANSFER >> | |
sleep(0.1) | |
ap.keys.send('UI_Up', hold=3) | |
sleep(0.1) | |
ap.keys.send('UI_Up') | |
sleep(0.1) | |
ap.keys.send('UI_Select') | |
ap.keys.send('UI_Select') | |
sleep(0.1) | |
ap.keys.send("UI_Back", repeat=4) | |
sleep(0.2) | |
ap.keys.send("HeadLookReset") | |
print("End of unload FC") | |
# quit() | |
def transfer_from_fleetcarrier(self, ap, buy_commodities): | |
""" Transfer specific good from Fleet Carrier to ship""" | |
self.ap_ckb('log+vce', f"Executing transfer from Fleet Carrier.") | |
logger.debug("transfer_to_fleetcarrier: entered") | |
# Go to the internal (right) panel inventory tab | |
self.show_inventory_tab() | |
# Assumes on the INVENTORY tab | |
ap.keys.send('UI_Right') | |
sleep(0.1) | |
ap.keys.send('UI_Up') # To FILTERS | |
sleep(0.1) | |
ap.keys.send('UI_Right') # To >> TRANSFER | |
sleep(0.1) | |
ap.keys.send('UI_Select') # Click >> TRANSFER | |
sleep(0.1) | |
ap.keys.send('UI_Up', hold=3) # go to top of list | |
sleep(0.1) | |
index = buy_commodities['Down'] | |
ap.keys.send('UI_Down', hold=0.05, repeat=index) # go down # of times user specified | |
sleep(0.5) | |
ap.keys.send('UI_Left', hold=10) # Transfer commodity, wait 10 sec to xfer | |
sleep(0.1) | |
ap.keys.send('UI_Select') # Take us down to "Confirm Item Transfer" | |
ap.keys.send('UI_Select') # Click Transfer | |
sleep(0.1) | |
ap.keys.send("UI_Back", repeat=4) | |
sleep(0.2) | |
ap.keys.send("HeadLookReset") | |
print("End of transfer from FC") | |
# Usage Example | |
if __name__ == "__main__": | |
logger.setLevel(logging.DEBUG) # Default to log all debug when running this file. | |
scr = Screen(cb=None) | |
mykeys = EDKeys(cb=None) | |
mykeys.activate_window = True # Helps with single steps testing | |
nav_pnl = EDInternalStatusPanel(scr, mykeys, None, None) | |
nav_pnl.show_inventory_tab() | |
--- | |
File: /EDJournal.py | |
--- | |
from __future__ import annotations | |
import os | |
from os import environ, listdir | |
from os.path import join, isfile, getmtime, abspath | |
from json import loads | |
from time import sleep, time | |
from datetime import datetime | |
from EDAP_data import ship_size_map, ship_name_map | |
from EDlogger import logger | |
from WindowsKnownPaths import * | |
""" | |
File EDJournal.py (leveraged the EDAutopilot on github, turned into a | |
class and enhanced, see https://github.com/skai2/EDAutopilot | |
Description: This file perform journal file processing. It opens the latest updated Journal* | |
file in the Saved directory, loads in the entries. Specific entries are stored in a dictionary. | |
Every time the dictionary is access the file will be read and if new lines exist those will be loaded | |
and parsed. | |
The dictionary can be accesses via: | |
jn = EDJournal() | |
print("Ship = ", jn.ship_state()) | |
... jn.ship_state()['shieldsup'] | |
Design: | |
- open the file once | |
- when accessing a field in the ship_state() first see if more to read from open file, if so | |
process it | |
- also check if a new journal file present, if so close current one and open new one | |
Author: [email protected] | |
""" | |
""" | |
TODO: thinking self.ship()[name] uses the same names as in the journal, so can lookup same construct | |
""" | |
def get_ship_size(ship: str) -> str: | |
""" Gets the ship size from the journal ship name. | |
@ship: The ship name from the journal (i.e. 'diamondbackxl'). | |
@return: The ship size ('S', 'M', 'L' or '' if ship not found or size not valid). | |
""" | |
if ship.lower() in ship_size_map: | |
return ship_size_map[ship.lower()] | |
else: | |
return '' | |
def get_ship_fullname(ship: str) -> str: | |
""" Gets the ship full name from the journal ship name. | |
@ship: The ship name from the journal (i.e. 'diamondbackxl'). | |
@return: The ship full name ('Diamondback Explorer' or '' if ship not found). | |
""" | |
if ship.lower() in ship_name_map: | |
return ship_name_map[ship.lower()] | |
else: | |
return '' | |
def check_fuel_scoop(modules: list[dict[str, any]] | None) -> bool: | |
""" Gets whether the ship has a fuel scoop. | |
""" | |
# Default to fuel scoop fitted if modules is None | |
if modules is None: | |
return True | |
# Check all modules. Could just check the internals, but this is easier. | |
for module in modules: | |
if "fuelscoop" in module['Item'].lower(): | |
return True | |
return False | |
def check_adv_docking_computer(modules: list[dict[str, any]] | None) -> bool: | |
""" Gets whether the ship has an advanced docking computer. | |
Advanced docking computer will dock and undock automatically. | |
""" | |
# Default to docking computer fitted if modules is None | |
if modules is None: | |
return True | |
# Check all modules. Could just check the internals, but this is easier. | |
for module in modules: | |
if "dockingcomputer_advanced" in module['Item'].lower(): | |
return True | |
return False | |
def check_std_docking_computer(modules: list[dict[str, any]] | None) -> bool: | |
""" Gets whether the ship has a standard docking computer. | |
Standard docking computer will dock automatically, but not undock. | |
""" | |
# Default to docking computer fitted if modules is None | |
if modules is None: | |
return True | |
# Check all modules. Could just check the internals, but this is easier. | |
for module in modules: | |
if "dockingcomputer_standard" in module['Item'].lower(): | |
return True | |
return False | |
def check_sco_fsd(modules: list[dict[str, any]] | None) -> bool: | |
""" Gets whether the ship has an FSD with SCO. | |
""" | |
# Default to SCO fitted if modules is None | |
if modules is None: | |
return True | |
# Check all modules. Could just check the internals, but this is easier. | |
for module in modules: | |
if module['Slot'] == "FrameShiftDrive": | |
if "overcharge" in module['Item'].lower(): | |
#print("FrameShiftDrive has SCO!") | |
return True | |
#print("FrameShiftDrive has no SCO") | |
return False | |
class EDJournal: | |
def __init__(self): | |
self.last_mod_time = None | |
self.log_file = None | |
self.current_log = self.get_latest_log() | |
self.open_journal(self.current_log) | |
self.ship = { | |
'time': (datetime.now() - datetime.fromtimestamp(getmtime(self.current_log))).seconds, | |
'odyssey': True, | |
'status': 'in_space', | |
'type': None, | |
'location': None, | |
'star_class': None, | |
'target': None, | |
'fighter_destroyed': False, | |
'shieldsup': True, | |
'under_attack': None, | |
'interdicted': False, | |
'no_dock_reason': None, | |
'mission_completed': 0, | |
'mission_redirected': 0, | |
'body': None, | |
'dist_jumped': 0, | |
'jumps_remains': 0, | |
'fuel_capacity': None, | |
'fuel_level': None, | |
'fuel_percent': None, | |
'is_scooping': False, | |
'cur_star_system': "", | |
'cur_station': "", | |
'cur_station_type': "", | |
'cargo_capacity': None, | |
'ship_size': None, | |
'has_fuel_scoop': None, | |
'SupercruiseDestinationDrop_type': None, | |
'has_adv_dock_comp': None, | |
'has_std_dock_comp': None, | |
'has_sco_fsd': None, | |
'StationServices': None, | |
} | |
self.ship_state() # load up from file | |
self.reset_items() | |
def get_file_modified_time(self) -> float: | |
return os.path.getmtime(self.current_log) | |
# these items do not have respective log entries to clear them. After initial reading of log file, clear these items | |
# also the App will need to reset these to False after detecting they were True | |
def reset_items(self): | |
self.ship['under_attack'] = False | |
self.ship['fighter_destroyed'] = False | |
def get_latest_log(self, path_logs=None): | |
"""Returns the full path of the latest (most recent) elite log file (journal) from specified path""" | |
if not path_logs: | |
path_logs = get_path(FOLDERID.SavedGames, UserHandle.current) + "\Frontier Developments\Elite Dangerous" | |
list_of_logs = [join(path_logs, f) for f in listdir(path_logs) if isfile(join(path_logs, f)) and f.startswith('Journal.')] | |
if not list_of_logs: | |
return None | |
latest_log = max(list_of_logs, key=getmtime) | |
return latest_log | |
def open_journal(self, log_name): | |
# if journal file is open then close it | |
if self.log_file is not None: | |
self.log_file.close() | |
logger.info("Opening new Journal: "+log_name) | |
# open the latest journal | |
self.log_file = open(log_name, encoding="utf-8") | |
self.last_mod_time = None | |
def parse_line(self, log): | |
# parse data | |
try: | |
# parse ship status | |
log_event = log['event'] | |
# If fileheader, pull whether running Odyssey or Horizons | |
if log_event == 'Fileheader': | |
#self.ship['odyssey'] = log['Odyssey'] | |
self.ship['odyssey'] = True # hardset to true for ED 4.0 since menus now same for Horizon | |
elif log_event == 'ShieldState': | |
if log['ShieldsUp'] == True: | |
self.ship['shieldsup'] = True | |
else: | |
self.ship['shieldsup'] = False | |
elif log_event == 'UnderAttack': | |
self.ship['under_attack'] = True | |
elif log_event == 'FighterDestroyed': | |
self.ship['fighter_destroyed'] = True | |
elif log_event == 'MissionCompleted': | |
self.ship['mission_completed'] = self.ship['mission_completed'] + 1 | |
elif log_event == 'MissionRedirected': | |
self.ship['mission_redirected'] = self.ship['mission_redirected'] + 1 | |
elif log_event == 'StartJump': | |
self.ship['status'] = str('starting_'+log['JumpType']).lower() | |
self.ship['SupercruiseDestinationDrop_type'] = None | |
if log['JumpType'] == 'Hyperspace': | |
self.ship['star_class'] = log['StarClass'] | |
elif log_event == 'SupercruiseEntry' or log_event == 'FSDJump': | |
self.ship['status'] = 'in_supercruise' | |
elif log_event == "DockingGranted": | |
self.ship['status'] = 'dockinggranted' | |
elif log_event == "DockingDenied": | |
self.ship['status'] = 'dockingdenied' | |
self.ship['no_dock_reason'] = log['Reason'] | |
elif log_event == 'SupercruiseExit': | |
self.ship['status'] = 'in_space' | |
self.ship['body'] = log['Body'] | |
elif log_event == 'SupercruiseDestinationDrop': | |
self.ship['SupercruiseDestinationDrop_type'] = log['Type'] | |
elif log_event == 'DockingCancelled': | |
self.ship['status'] = 'in_space' | |
elif log_event == 'Undocked': | |
self.ship['status'] = 'starting_undocking' | |
#self.ship['status'] = 'in_space' | |
elif log_event == 'DockingRequested': | |
self.ship['status'] = 'starting_docking' | |
elif log_event == "Music" and log['MusicTrack'] == "DockingComputer": | |
if self.ship['status'] == 'starting_undocking': | |
self.ship['status'] = 'in_undocking' | |
elif self.ship['status'] == 'starting_docking': | |
self.ship['status'] = 'in_docking' | |
elif log_event == "Music" and log['MusicTrack'] == "NoTrack" and self.ship['status'] == 'in_undocking': | |
self.ship['status'] = 'in_space' | |
# for unodck from outpost | |
elif log_event == "Music" and log['MusicTrack'] == "Exploration" and self.ship['status'] == 'in_undocking': | |
self.ship['status'] = 'in_space' | |
elif log_event == 'Docked': | |
# {"timestamp": "2024-09-29T00:47:08Z", "event": "Docked", "StationName": "Filipchenko City", | |
# "StationType": "Coriolis", "Taxi": false, "Multicrew": false, "StarSystem": "G 139-50", | |
# "SystemAddress": 13864557225401, "MarketID": 3229027584, | |
# "StationFaction": {"Name": "Pixel Bandits Security Force"}, | |
# "StationGovernment": "$government_Democracy;", "StationGovernment_Localised": "Democracy", | |
# "StationServices": ["dock", "autodock", "blackmarket", "commodities", "contacts", "exploration", | |
# "missions", "outfitting", "crewlounge", "rearm", "refuel", "repair", "shipyard", | |
# "tuning", "engineer", "missionsgenerated", "flightcontroller", "stationoperations", | |
# "powerplay", "searchrescue", "materialtrader", "stationMenu", "shop", "livery", | |
# "socialspace", "bartender", "vistagenomics", "pioneersupplies", "apexinterstellar", | |
# "frontlinesolutions"], "StationEconomy": "$economy_HighTech;", | |
# "StationEconomy_Localised": "High Tech", "StationEconomies": [ | |
# {"Name": "$economy_HighTech;", "Name_Localised": "High Tech", "Proportion": 0.800000}, | |
# {"Name": "$economy_Refinery;", "Name_Localised": "Refinery", "Proportion": 0.200000}], | |
# "DistFromStarLS": 6.950547, "LandingPads": {"Small": 6, "Medium": 12, "Large": 7}} | |
self.ship['status'] = 'in_station' | |
self.ship['location'] = log['StarSystem'] | |
self.ship['cur_star_system'] = log['StarSystem'] | |
self.ship['cur_station'] = log['StationName'] | |
self.ship['cur_station_type'] = log['StationType'] | |
self.ship['StationServices'] = log['StationServices'] | |
# parse location | |
elif log_event == 'Location': | |
self.ship['location'] = log['StarSystem'] | |
self.ship['cur_star_system'] = log['StarSystem'] | |
self.ship['cur_station'] = log['StationName'] | |
self.ship['cur_station_type'] = log['StationType'] | |
if log['Docked'] == True: | |
self.ship['status'] = 'in_station' | |
elif log_event == 'Interdicted': | |
self.ship['interdicted'] = True | |
# parse ship type | |
elif log_event == 'LoadGame': | |
self.ship['type'] = log['Ship'].lower() | |
self.ship['ship_size'] = get_ship_size(log['Ship']) | |
# Parse Loadout | |
# When written: at startup, when loading from main menu, or when switching ships, | |
# or after changing the ship in Outfitting, or when docking SRV back in mothership | |
elif log_event == 'Loadout': | |
self.ship['type'] = log['Ship'].lower() | |
self.ship['ship_size'] = get_ship_size(log['Ship']) | |
self.ship['cargo_capacity'] = log['CargoCapacity'] | |
self.ship['has_fuel_scoop'] = check_fuel_scoop(log['Modules']) | |
self.ship['has_adv_dock_comp'] = check_adv_docking_computer(log['Modules']) | |
self.ship['has_std_dock_comp'] = check_std_docking_computer(log['Modules']) | |
self.ship['has_sco_fsd'] = check_sco_fsd(log['Modules']) | |
# parse fuel | |
if 'FuelLevel' in log and self.ship['type'] != 'TestBuggy': | |
self.ship['fuel_level'] = log['FuelLevel'] | |
if 'FuelCapacity' in log and self.ship['type'] != 'TestBuggy': | |
try: | |
self.ship['fuel_capacity'] = log['FuelCapacity']['Main'] | |
except: | |
self.ship['fuel_capacity'] = log['FuelCapacity'] | |
if log_event == 'FuelScoop' and 'Total' in log: | |
self.ship['fuel_level'] = log['Total'] | |
if self.ship['fuel_level'] and self.ship['fuel_capacity']: | |
self.ship['fuel_percent'] = round((self.ship['fuel_level'] / self.ship['fuel_capacity'])*100) | |
else: | |
self.ship['fuel_percent'] = 10 | |
# parse scoop | |
# | |
if log_event == 'FuelScoop' and self.ship['time'] < 10 and self.ship['fuel_percent'] < 100: | |
self.ship['is_scooping'] = True | |
else: | |
self.ship['is_scooping'] = False | |
if log_event == 'FSDJump': | |
self.ship['location'] = log['StarSystem'] | |
self.ship['cur_star_system'] = log['StarSystem'] | |
#TODO if 'StarClass' in log: | |
#TODO self.ship['star_class'] = log['StarClass'] | |
# parse target | |
if log_event == 'FSDTarget': | |
if log['Name'] == self.ship['location']: | |
self.ship['target'] = None | |
self.ship['jumps_remains'] = 0 | |
else: | |
self.ship['target'] = log['Name'] | |
try: | |
self.ship['jumps_remains'] = log['RemainingJumpsInRoute'] | |
except: | |
pass | |
# | |
# 'Log did not have jumps remaining. This happens most if you have less than .' + | |
# '3 jumps remaining. Jumps remaining will be inaccurate for this jump.') | |
elif log_event == 'FSDJump': | |
if self.ship['location'] == self.ship['target']: | |
self.ship['target'] = None | |
self.ship['dist_jumped'] = log["JumpDist"] | |
# parse nav route clear | |
elif log_event == 'NavRouteClear': | |
self.ship['target'] = None | |
self.ship['jumps_remains'] = 0 | |
elif log_event == 'CarrierJump': | |
self.ship['location'] = log['StarSystem'] | |
self.ship['cur_star_system'] = log['StarSystem'] | |
self.ship['cur_station'] = log['StationName'] | |
self.ship['cur_station_type'] = log['StationType'] | |
# exceptions | |
except Exception as e: | |
#logger.exception("Exception occurred") | |
print(e) | |
def ship_state(self): | |
latest_log = self.get_latest_log() | |
# open journal file if not open yet or there is a more recent journal | |
if self.current_log is None or self.current_log != latest_log: | |
self.open_journal(latest_log) | |
# Check if file changed | |
if self.get_file_modified_time() == self.last_mod_time: | |
return self.ship | |
cnt = 0 | |
while True: | |
line = self.log_file.readline() | |
# if end of file then break from while True | |
if not line: | |
break | |
else: | |
log = loads(line) | |
cnt = cnt + 1 | |
current_jrnl = self.ship.copy() | |
self.parse_line(log) | |
if self.ship != current_jrnl: | |
logger.debug('Journal*.log: read: '+str(cnt)+' ship: '+str(self.ship)) | |
self.last_mod_time = self.get_file_modified_time() | |
return self.ship | |
def main(): | |
jn = EDJournal() | |
while True: | |
sleep(5) | |
print("Ship = ", jn.ship_state()) | |
if __name__ == "__main__": | |
main() | |
--- | |
File: /EDKeys.py | |
--- | |
from __future__ import annotations | |
import json | |
from os import environ, listdir | |
import os | |
from os.path import getmtime, isfile, join | |
from time import sleep | |
from typing import Any, final | |
from xml.etree.ElementTree import parse | |
import win32gui | |
import xmltodict | |
from directinput import * | |
from EDlogger import logger | |
""" | |
Description: Pulls the keybindings for specific controls from the ED Key Bindings file, this class also | |
has method for sending a key to the display that has focus (so must have ED with focus) | |
Constraints: This file will use the latest modified *.binds file | |
""" | |
def set_focus_elite_window(): | |
""" set focus to the ED window, if ED does not have focus then the keystrokes will go to the window | |
that does have focus. """ | |
handle = win32gui.FindWindow(0, "Elite - Dangerous (CLIENT)") | |
if handle != 0: | |
win32gui.SetForegroundWindow(handle) # give focus to ED | |
@final | |
class EDKeys: | |
def __init__(self, cb): | |
self.ap_ckb = cb | |
self.key_mod_delay = 0.010 | |
self.key_default_delay = 0.200 | |
self.key_repeat_delay = 0.100 | |
self.keys_to_obtain = [ | |
'YawLeftButton', | |
'YawRightButton', | |
'RollLeftButton', | |
'RollRightButton', | |
'PitchUpButton', | |
'PitchDownButton', | |
'SetSpeedZero', | |
'SetSpeed50', | |
'SetSpeed100', | |
'HyperSuperCombination', | |
'SelectTarget', | |
'DeployHeatSink', | |
'UIFocus', | |
'UI_Up', | |
'UI_Down', | |
'UI_Left', | |
'UI_Right', | |
'UI_Select', | |
'UI_Back', | |
'CycleNextPanel', | |
'HeadLookReset', | |
'PrimaryFire', | |
'SecondaryFire', | |
'ExplorationFSSEnter', | |
'ExplorationFSSQuit', | |
'MouseReset', | |
'DeployHardpointToggle', | |
'IncreaseEnginesPower', | |
'IncreaseWeaponsPower', | |
'IncreaseSystemsPower', | |
'GalaxyMapOpen', | |
'CamZoomIn', # Gal map zoom in | |
'SystemMapOpen', | |
'UseBoostJuice', | |
'Supercruise', | |
'UpThrustButton', | |
'LandingGearToggle', | |
'TargetNextRouteSystem', # Target next system in route | |
'CamTranslateForward', | |
'CamTranslateRight', | |
] | |
self.keys = self.get_bindings() | |
self.bindings = self.get_bindings_dict() | |
self.activate_window = False | |
self.missing_keys = [] | |
# We want to log the keyboard name instead of just the key number so we build a reverse dictionary | |
# so we can look up the name also | |
self.reversed_dict = {value: key for key, value in SCANCODE.items()} | |
# dump config to log | |
for key in self.keys_to_obtain: | |
try: | |
# lookup the keyname in the SCANCODE reverse dictionary and output that key name | |
keyname = self.reversed_dict.get(self.keys[key]['key'], "Key not found") | |
keymod = " " | |
# if key modifier, then look up that modifier name also | |
if len(self.keys[key]['mods']) != 0: | |
keymod = self.reversed_dict.get(self.keys[key]['mods'][0], " ") | |
logger.info('\tget_bindings_<{}>={} Key: <{}> Mod: <{}>'.format(key, self.keys[key], keyname, keymod)) | |
if key not in self.keys: | |
self.ap_ckb('log', | |
f"WARNING: \tget_bindings_<{key}>= does not have a valid keyboard keybind {keyname}.") | |
logger.warning( | |
"\tget_bindings_<{}>= does not have a valid keyboard keybind {}".format(key, keyname).upper()) | |
self.missing_keys.append(key) | |
except Exception as e: | |
self.ap_ckb('log', f"WARNING: \tget_bindings_<{key}>= does not have a valid keyboard keybind.") | |
logger.warning("\tget_bindings_<{}>= does not have a valid keyboard keybind.".format(key).upper()) | |
self.missing_keys.append(key) | |
# Check for known key collisions | |
collisions = self.get_collisions('UI_Up') | |
if 'CamTranslateForward' in collisions: | |
warn_text = ("Up arrow key is used for 'UI Panel Up' and 'Galaxy Cam Translate Fwd'. " | |
"This will cause problems in the Galaxy Map. Change the keybinding for " | |
"'Galaxy Cam Translate' to Shift + WASD under General Controls in ED Controls.") | |
self.ap_ckb('log', f"WARNING: {warn_text}") | |
logger.warning(f"{warn_text}") | |
collisions = self.get_collisions('UI_Right') | |
if 'CamTranslateRight' in collisions: | |
warn_text = ("Up arrow key is used for 'UI Panel Up' and 'Galaxy Cam Translate Right'. " | |
"This will cause problems in the Galaxy Map. Change the keybinding for" | |
" 'Galaxy Cam Translate' to Shift + WASD under General Controls in ED Controls.") | |
self.ap_ckb('log', f"WARNING: {warn_text}") | |
logger.warning(f"{warn_text}") | |
# Check if the hotkeys are used in ED | |
binding_name = self.check_hotkey_in_bindings('Key_End') | |
if binding_name != "": | |
warn_text = (f"Hotkey 'Key_End' is used in the ED keybindings for '{binding_name}'. Recommend changing in" | |
f" ED to another key to avoid EDAP accidentally being triggered.") | |
self.ap_ckb('log', f"WARNING: {warn_text}") | |
logger.warning(f"{warn_text}") | |
binding_name = self.check_hotkey_in_bindings('Key_Insert') | |
if binding_name != "": | |
warn_text = (f"Hotkey 'Key_Insert' is used in the ED keybindings for '{binding_name}'. Recommend changing in" | |
f" ED to another key to avoid EDAP accidentally being triggered.") | |
self.ap_ckb('log', f"WARNING: {warn_text}") | |
logger.warning(f"{warn_text}") | |
binding_name = self.check_hotkey_in_bindings('Key_PageUp') | |
if binding_name != "": | |
warn_text = (f"Hotkey 'Key_PageUp' is used in the ED keybindings for '{binding_name}'. Recommend changing in" | |
f" ED to another key to avoid EDAP accidentally being triggered.") | |
self.ap_ckb('log', f"WARNING: {warn_text}") | |
logger.warning(f"{warn_text}") | |
binding_name = self.check_hotkey_in_bindings('Key_Home') | |
if binding_name != "": | |
warn_text = (f"Hotkey 'Key_Home' is used in the ED keybindings for '{binding_name}'. Recommend changing in" | |
f" ED to another key to avoid EDAP accidentally being triggered.") | |
self.ap_ckb('log', f"WARNING: {warn_text}") | |
logger.warning(f"{warn_text}") | |
def get_bindings(self) -> dict[str, Any]: | |
"""Returns a dict struct with the direct input equivalent of the necessary elite keybindings""" | |
direct_input_keys = {} | |
latest_bindings = self.get_latest_keybinds() | |
if not latest_bindings: | |
return {} | |
bindings_tree = parse(latest_bindings) | |
bindings_root = bindings_tree.getroot() | |
for item in bindings_root: | |
if item.tag in self.keys_to_obtain: | |
key = None | |
mods = [] | |
hold = None | |
# Check primary | |
if item[0].attrib['Device'].strip() == "Keyboard": | |
key = item[0].attrib['Key'] | |
for modifier in item[0]: | |
if modifier.tag == "Modifier": | |
mods.append(modifier.attrib['Key']) | |
elif modifier.tag == "Hold": | |
hold = True | |
# Check secondary (and prefer secondary) | |
if item[1].attrib['Device'].strip() == "Keyboard": | |
key = item[1].attrib['Key'] | |
mods = [] | |
hold = None | |
for modifier in item[1]: | |
if modifier.tag == "Modifier": | |
mods.append(modifier.attrib['Key']) | |
elif modifier.tag == "Hold": | |
hold = True | |
# Prepare final binding | |
binding: None | dict[str, Any] = None | |
try: | |
if key is not None: | |
binding = {} | |
binding['key'] = SCANCODE[key] | |
binding['mods'] = [] | |
for mod in mods: | |
binding['mods'].append(SCANCODE[mod]) | |
if hold is not None: | |
binding['hold'] = True | |
except KeyError: | |
print("Unrecognised key '" + ( | |
json.dumps(binding) if binding else '?') + "' for bind '" + item.tag + "'") | |
if binding is not None: | |
direct_input_keys[item.tag] = binding | |
if len(list(direct_input_keys.keys())) < 1: | |
return {} | |
else: | |
return direct_input_keys | |
def get_bindings_dict(self) -> dict[str, Any]: | |
"""Returns a dict of all the elite keybindings. | |
@return: A dictionary of the keybinds file. | |
Example: | |
{ | |
'Root': { | |
'YawLeftButton': { | |
'Primary': { | |
'@Device': 'Keyboard', | |
'@Key': 'Key_A' | |
}, | |
'Secondary': { | |
'@Device': '{NoDevice}', | |
'@Key': '' | |
} | |
} | |
} | |
} | |
""" | |
latest_bindings = self.get_latest_keybinds() | |
if not latest_bindings: | |
return {} | |
try: | |
with open(latest_bindings, 'r') as file: | |
my_xml = file.read() | |
my_dict = xmltodict.parse(my_xml) | |
return my_dict | |
except OSError as e: | |
logger.error(f"OS Error reading Elite Dangerous bindings file: {latest_bindings}.") | |
raise Exception(f"OS Error reading Elite Dangerous bindings file: {latest_bindings}.") | |
def check_hotkey_in_bindings(self, key_name: str) -> str: | |
""" Check for the action keys. """ | |
ret = [] | |
for key, value in self.bindings['Root'].items(): | |
if type(value) is dict: | |
primary = value.get('Primary', None) | |
if primary is not None: | |
if primary['@Key'] == key_name: | |
ret.append(f"{key} (Primary)") | |
secondary = value.get('Secondary', None) | |
if secondary is not None: | |
if secondary['@Key'] == key_name: | |
ret.append(f"{key} (Secondary)") | |
return " and ".join(ret) | |
# Note: this routine will grab the *.binds file which is the latest modified | |
def get_latest_keybinds(self): | |
path_bindings = environ['LOCALAPPDATA'] + "\Frontier Developments\Elite Dangerous\Options\Bindings" | |
try: | |
list_of_bindings = [join(path_bindings, f) for f in listdir(path_bindings) if | |
isfile(join(path_bindings, f)) and f.endswith('.binds')] | |
except FileNotFoundError as e: | |
return None | |
if not list_of_bindings: | |
return None | |
latest_bindings = max(list_of_bindings, key=getmtime) | |
logger.info(f'Latest keybindings file:{latest_bindings}') | |
return latest_bindings | |
def send_key(self, type, key): | |
# Focus Elite window if configured | |
if self.activate_window: | |
set_focus_elite_window() | |
sleep(0.05) | |
if type == 'Up': | |
ReleaseKey(key) | |
else: | |
PressKey(key) | |
def send(self, key_binding, hold=None, repeat=1, repeat_delay=None, state=None): | |
key = self.keys.get(key_binding) | |
if key is None: | |
logger.warning('SEND=NONE !!!!!!!!') | |
self.ap_ckb('log', f"WARNING: Unable to retrieve keybinding for {key_binding}.") | |
raise Exception( | |
f"Unable to retrieve keybinding for {key_binding}. Advise user to check game settings for keyboard bindings.") | |
key_name = self.reversed_dict.get(key['key'], "Key not found") | |
logger.debug('\tsend=' + key_binding + ',key:' + str(key) + ',key_name:' + key_name + ',hold:' + str( | |
hold) + ',repeat:' + str( | |
repeat) + ',repeat_delay:' + str(repeat_delay) + ',state:' + str(state)) | |
for i in range(repeat): | |
# Focus Elite window if configured. | |
if self.activate_window: | |
set_focus_elite_window() | |
sleep(0.05) | |
if state is None or state == 1: | |
for mod in key['mods']: | |
PressKey(mod) | |
sleep(self.key_mod_delay) | |
PressKey(key['key']) | |
if state is None: | |
if hold: | |
sleep(hold) | |
else: | |
sleep(self.key_default_delay) | |
if 'hold' in key: | |
sleep(0.1) | |
if state is None or state == 0: | |
ReleaseKey(key['key']) | |
for mod in key['mods']: | |
sleep(self.key_mod_delay) | |
ReleaseKey(mod) | |
if repeat_delay: | |
sleep(repeat_delay) | |
else: | |
sleep(self.key_repeat_delay) | |
def get_collisions(self, key_name: str) -> list[str]: | |
""" Get key name collisions (keys used for more than one binding). | |
@param key_name: The key name (i.e. UI_Up, UI_Down). | |
""" | |
key = self.keys.get(key_name) | |
collisions = [] | |
for k, v in self.keys.items(): | |
if key == v: | |
collisions.append(k) | |
return collisions | |
--- | |
File: /EDlogger.py | |
--- | |
import colorlog | |
import datetime | |
import logging | |
import os | |
from pathlib import Path | |
_filename = 'autopilot.log' | |
# Rename existing log file to create new one. | |
if os.path.exists(_filename): | |
filename_only = Path(_filename).stem | |
t = os.path.getmtime(_filename) | |
v = datetime.datetime.fromtimestamp(t) | |
x = v.strftime('%Y-%m-%d %H-%M-%S') | |
os.rename(_filename, f"{filename_only} {x}.log") | |
# Define the logging config. | |
logging.basicConfig(filename=_filename, level=logging.ERROR, | |
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', | |
datefmt='%H:%M:%S') | |
logger = colorlog.getLogger('ed_log') | |
# Change this to debug if want to see debug lines in log file | |
logger.setLevel(logging.WARNING) # change to INFO for more... DEBUG for much more | |
handler = logging.StreamHandler() | |
handler.setLevel(logging.WARNING) # change this to what is shown on console | |
handler.setFormatter( | |
colorlog.ColoredFormatter('%(log_color)s%(levelname)-8s%(reset)s %(white)s%(message)s', | |
log_colors={ | |
'DEBUG': 'fg_bold_cyan', | |
'INFO': 'fg_bold_green', | |
'WARNING': 'bg_bold_yellow,fg_bold_blue', | |
'ERROR': 'bg_bold_red,fg_bold_white', | |
'CRITICAL': 'bg_bold_red,fg_bold_yellow', | |
},secondary_log_colors={} | |
)) | |
logger.addHandler(handler) | |
#logger.disabled = True | |
#logger.disabled = False | |
--- | |
File: /EDShipControl.py | |
--- | |
from __future__ import annotations | |
from EDAP_data import * | |
from OCR import OCR | |
from StatusParser import StatusParser | |
class EDShipControl: | |
""" Handles ship control, FSD, SC, etc. """ | |
def __init__(self, screen, keys, cb): | |
self.screen = screen | |
self.ocr = OCR(screen) | |
self.keys = keys | |
self.status_parser = StatusParser() | |
self.ap_ckb = cb | |
def goto_cockpit_view(self) -> bool: | |
""" Goto cockpit view. | |
@return: True once complete. | |
""" | |
if self.status_parser.get_gui_focus() == GuiFocusNoFocus: | |
return True | |
# Go down to cockpit view | |
while not self.status_parser.get_gui_focus() == GuiFocusNoFocus: | |
self.keys.send("UI_Back") # make sure back in cockpit view | |
return True | |
--- | |
File: /EDStationServicesInShip.py | |
--- | |
from __future__ import annotations | |
from MarketParser import MarketParser | |
from OCR import OCR | |
from StatusParser import StatusParser | |
from time import sleep | |
from EDlogger import logger | |
class EDStationServicesInShip: | |
""" Handles Station Services In Ship. """ | |
def __init__(self, ed_ap, screen, keys, cb): | |
self.ap = ed_ap | |
self.screen = screen | |
self.ocr = OCR(screen) | |
self.keys = keys | |
self.status_parser = StatusParser() | |
self.ap_ckb = cb | |
self.market_parser = MarketParser() | |
def goto_station_services(self) -> bool: | |
""" Goto Station Services. """ | |
# Go to cockpit view | |
self.ap.ship_control.goto_cockpit_view() | |
self.keys.send("UI_Up", repeat=3) # go to very top (refuel line) | |
self.keys.send("UI_Down") # station services | |
self.keys.send("UI_Select") # station services | |
# TODO - replace with OCR from OCR branch | |
sleep(5) # wait for new menu to finish rendering | |
return True | |
def select_buy(self, keys) -> bool: | |
""" Select Buy. Assumes on Commodities Market screen. """ | |
# Select Buy | |
keys.send("UI_Left", repeat=2) | |
keys.send("UI_Up", repeat=4) | |
keys.send("UI_Select") # Select Buy | |
sleep(0.5) # give time to bring up list | |
keys.send('UI_Right') # Go to top of commodities list | |
return True | |
def select_sell(self, keys) -> bool: | |
""" Select Buy. Assumes on Commodities Market screen. """ | |
# Select Buy | |
keys.send("UI_Left", repeat=2) | |
keys.send("UI_Up", repeat=4) | |
keys.send("UI_Down") | |
keys.send("UI_Select") # Select Sell | |
sleep(0.5) # give time to bring up list | |
keys.send('UI_Right') # Go to top of commodities list | |
return True | |
def buy_commodity(self, keys, name: str, qty: int, free_cargo: int) -> tuple[bool, int]: | |
""" Buy qty of commodity. If qty >= 9999 then buy as much as possible. | |
Assumed to be in the commodities buy screen in the list. """ | |
# If we are updating requirement count, me might have all the qty we need | |
if qty <= 0: | |
return False, 0 | |
# Determine if station sells the commodity! | |
self.market_parser.get_market_data() | |
if not self.market_parser.can_buy_item(name): | |
self.ap_ckb('log+vce', f"'{name}' is not sold or has no stock at {self.market_parser.get_market_name()}.") | |
logger.debug(f"Item '{name}' is not sold or has no stock at {self.market_parser.get_market_name()}.") | |
return False, 0 | |
# Find commodity in market and return the index | |
buyable_items = self.market_parser.get_buyable_items() | |
index = -1 | |
stock = 0 | |
for i, value in enumerate(buyable_items): | |
if value['Name_Localised'].upper() == name.upper(): | |
index = i | |
stock = value['Stock'] | |
logger.debug(f"Execute trade: Buy {name} (want {qty} of {stock} avail.) at position {index + 1}.") | |
break | |
# Actual qty we can sell | |
act_qty = min(qty, stock, free_cargo) | |
# See if we buy all and if so, remove the item to update the list, as the item will be removed | |
# from the commodities screen, but the market.json will not be updated. | |
buy_all = act_qty == stock | |
if buy_all: | |
for i, value in enumerate(self.market_parser.current_data['Items']): | |
if value['Name_Localised'].upper() == name.upper(): | |
# Set the stock bracket to 0, so it does not get included in available commodities list. | |
self.market_parser.current_data['Items'][i]['StockBracket'] = 0 | |
if index > -1: | |
keys.send('UI_Up', hold=3.0) # go up to top of list | |
keys.send('UI_Down', hold=0.05, repeat=index) # go down # of times user specified | |
sleep(0.5) | |
keys.send('UI_Select') # Select that commodity | |
sleep(0.5) # give time to popup | |
keys.send('UI_Up', repeat=2) # go up to quantity to buy (may not default to this) | |
# Log the planned quantity | |
self.ap_ckb('log+vce', f"Buying {act_qty} units of {name}.") | |
logger.info(f"Attempting to buy {act_qty} units of {name}") | |
# Increment count | |
if qty >= 9999 or qty >= stock or qty >= free_cargo: | |
keys.send("UI_Right", hold=4) | |
else: | |
keys.send("UI_Right", hold=0.04, repeat=act_qty) | |
keys.send('UI_Down') | |
keys.send('UI_Select') # Select Buy | |
sleep(0.5) | |
# keys.send('UI_Back') # Back to commodities list | |
return True, act_qty | |
def sell_commodity(self, keys, name: str, qty: int, cargo_parser) -> tuple[bool, int]: | |
""" Sell qty of commodity. If qty >= 9999 then sell as much as possible. | |
Assumed to be in the commodities sell screen in the list. | |
@param keys: Keys class for sending keystrokes. | |
@param name: Name of the commodity. | |
@param qty: Quantity to sell. | |
@param cargo_parser: Current cargo to check if rare or demand=1 items exist in hold. | |
@return: Sale successful (T/F) and Qty. | |
""" | |
# If we are updating requirement count, me might have sold all we have | |
if qty <= 0: | |
return False, 0 | |
# Determine if station buys the commodity! | |
self.market_parser.get_market_data() | |
if not self.market_parser.can_sell_item(name): | |
self.ap_ckb('log+vce', f"'{name}' is not bought at {self.market_parser.get_market_name()}.") | |
logger.debug(f"Item '{name}' is not bought at {self.market_parser.get_market_name()}.") | |
return False, 0 | |
# Find commodity in market and return the index | |
sellable_items = self.market_parser.get_sellable_items(cargo_parser) | |
index = -1 | |
demand = 0 | |
for i, value in enumerate(sellable_items): | |
if value['Name_Localised'].upper() == name.upper(): | |
index = i | |
demand = value['Demand'] | |
logger.debug(f"Execute trade: Sell {name} ({qty} of {demand} demanded) at position {index + 1}.") | |
break | |
# Qty we can sell. Unlike buying, we can sell more than the demand | |
# But maybe not at all stations! | |
act_qty = qty | |
if index > -1: | |
keys.send('UI_Up', hold=3.0) # go up to top of list | |
keys.send('UI_Down', hold=0.05, repeat=index) # go down # of times user specified | |
sleep(0.5) | |
keys.send('UI_Select') # Select that commodity | |
sleep(0.5) # give time for popup | |
keys.send('UI_Up', repeat=2) # make sure at top | |
# Log the planned quantity | |
if qty >= 9999: | |
self.ap_ckb('log+vce', f"Selling all our units of {name}.") | |
logger.info(f"Attempting to sell all our units of {name}") | |
keys.send("UI_Right", hold=4) | |
else: | |
self.ap_ckb('log+vce', f"Selling {act_qty} units of {name}.") | |
logger.info(f"Attempting to sell {act_qty} units of {name}") | |
keys.send('UI_Left', hold=4.0) # Clear quantity to 0 | |
keys.send("UI_Right", hold=0.04, repeat=act_qty) | |
keys.send('UI_Down') # Down to the Sell button (already assume sell all) | |
keys.send('UI_Select') # Select to Sell all | |
sleep(0.5) | |
# keys.send('UI_Back') # Back to commodities list | |
return True, act_qty | |
--- | |
File: /EDSystemMap.py | |
--- | |
from __future__ import annotations | |
from EDAP_data import GuiFocusSystemMap | |
from EDlogger import logger | |
from OCR import OCR | |
from StatusParser import StatusParser | |
from time import sleep | |
class EDSystemMap: | |
""" Handles the System Map. """ | |
def __init__(self, ed_ap, screen, keys, cb, is_odyssey=True): | |
self.ap = ed_ap | |
self.is_odyssey = is_odyssey | |
self.screen = screen | |
self.ocr = OCR(screen) | |
self.keys = keys | |
self.status_parser = StatusParser() | |
self.ap_ckb = cb | |
def set_sys_map_dest_bookmark(self, ap, bookmark_type: str, bookmark_position: int) -> bool: | |
""" Set the System Map destination using a bookmark. | |
@param ap: ED_AP reference. | |
@param bookmark_type: The bookmark type (Favorite, Body, Station, Settlement or Navigation), Favorite | |
being the default if no match is made with the other options. Navigation is unique in that it uses | |
the Nav Panel instead of the System Map. | |
@param bookmark_position: The position in the bookmark list, starting at 1 for the first bookmark. | |
@return: True if bookmark could be selected, else False | |
""" | |
if self.is_odyssey and bookmark_position != -1: | |
# Check if this is a nav-panel bookmark | |
if not bookmark_type.lower().startswith("nav"): | |
self.goto_system_map() | |
ap.keys.send('UI_Left') # Go to BOOKMARKS | |
sleep(.5) | |
ap.keys.send('UI_Select') # Select BOOKMARKS | |
sleep(.25) | |
ap.keys.send('UI_Right') # Go to FAVORITES | |
sleep(.25) | |
# If bookmark type is Fav, do nothing as this is the first item | |
if bookmark_type.lower().startswith("bod"): | |
ap.keys.send('UI_Down', repeat=1) # Go to BODIES | |
elif bookmark_type.lower().startswith("sta"): | |
ap.keys.send('UI_Down', repeat=2) # Go to STATIONS | |
elif bookmark_type.lower().startswith("set"): | |
ap.keys.send('UI_Down', repeat=3) # Go to SETTLEMENTS | |
sleep(.25) | |
ap.keys.send('UI_Select') # Select bookmark type, moves you to bookmark list | |
ap.keys.send('UI_Left') # Sometimes the first bookmark is not selected, so we try to force it. | |
ap.keys.send('UI_Right') | |
sleep(.25) | |
ap.keys.send('UI_Down', repeat=bookmark_position - 1) | |
sleep(.25) | |
ap.keys.send('UI_Select', hold=3.0) | |
# Close System Map | |
ap.keys.send('SystemMapOpen') | |
sleep(0.5) | |
return True | |
elif bookmark_type.lower().startswith("nav"): | |
# TODO - Move to, or call Nav Panel code instead? | |
# This is a nav-panel bookmark | |
# Goto cockpit view | |
self.ap.ship_control.goto_cockpit_view() | |
# get to the Left Panel menu: Navigation | |
ap.keys.send("HeadLookReset") | |
ap.keys.send("UIFocus", state=1) | |
ap.keys.send("UI_Left") | |
ap.keys.send("UIFocus", state=0) # this gets us over to the Nav panel | |
ap.keys.send('UI_Up', hold=4) | |
ap.keys.send('UI_Down', repeat=bookmark_position - 1) | |
sleep(1.0) | |
ap.keys.send('UI_Select') | |
sleep(0.25) | |
ap.keys.send('UI_Select') | |
ap.keys.send("UI_Back") | |
ap.keys.send("HeadLookReset") | |
return True | |
return False | |
def goto_system_map(self): | |
""" Open System Map if we are not there. | |
""" | |
if self.status_parser.get_gui_focus() != GuiFocusSystemMap: | |
logger.debug("Opening System Map") | |
# Goto cockpit view | |
self.ap.ship_control.goto_cockpit_view() | |
# Goto System Map | |
self.ap.keys.send('SystemMapOpen') | |
# TODO - Add OCR to check screen loaded | |
sleep(3.5) | |
else: | |
logger.debug("System Map is already open") | |
self.keys.send('UI_Left') | |
self.keys.send('UI_Up', hold=2) | |
self.keys.send('UI_Left') | |
--- | |
File: /EDWayPoint.py | |
--- | |
from __future__ import annotations | |
from time import sleep | |
from CargoParser import CargoParser | |
from EDAP_data import FlagsDocked | |
from EDKeys import EDKeys | |
from EDlogger import logger | |
import json | |
from MarketParser import MarketParser | |
from MousePt import MousePoint | |
from pathlib import Path | |
""" | |
File: EDWayPoint.py | |
Description: | |
Class will load file called waypoints.json which contains a list of System name to jump to. | |
Provides methods to select a waypoint pass into it. | |
Author: [email protected] | |
""" | |
class EDWayPoint: | |
def __init__(self, ed_ap, is_odyssey=True): | |
self.ap = ed_ap | |
self.is_odyssey = is_odyssey | |
self.filename = './waypoints.json' | |
self.stats_log = {'Colonisation': 0, 'Construction': 0, 'Fleet Carrier': 0, 'Station': 0} | |
self.waypoints = {} | |
# { "Ninabin": {"DockWithTarget": false, "TradeSeq": None, "Completed": false} } | |
# for i, key in enumerate(self.waypoints): | |
# self.waypoints[target]['DockWithTarget'] == True ... then go into SC Assist | |
# self.waypoints[target]['Completed'] == True | |
# if docked and self.waypoints[target]['Completed'] == False | |
# execute_seq(self.waypoints[target]['TradeSeq']) | |
ss = self.read_waypoints() | |
# if we read it then point to it, otherwise use the default table above | |
if ss is not None: | |
self.waypoints = ss | |
logger.debug("EDWayPoint: read json:" + str(ss)) | |
self.num_waypoints = len(self.waypoints) | |
# print("waypoints: "+str(self.waypoints)) | |
self.step = 0 | |
self.mouse = MousePoint() | |
self.market_parser = MarketParser() | |
self.cargo_parser = CargoParser() | |
def load_waypoint_file(self, filename=None) -> bool: | |
if filename is None: | |
return False | |
ss = self.read_waypoints(filename) | |
if ss is not None: | |
self.waypoints = ss | |
self.filename = filename | |
self.ap.ap_ckb('log', f"Loaded Waypoint file: {filename}") | |
logger.debug("EDWayPoint: read json:" + str(ss)) | |
return True | |
self.ap.ap_ckb('log', f"Waypoint file is invalid. Check log file for details.") | |
return False | |
def read_waypoints(self, filename='./waypoints/waypoints.json'): | |
s = None | |
try: | |
with open(filename, "r") as fp: | |
s = json.load(fp) | |
# Perform any checks on the data returned | |
# Check if the waypoint data contains the 'GlobalShoppingList' (new requirement) | |
if 'GlobalShoppingList' not in s: | |
# self.ap.ap_ckb('log', f"Waypoint file is invalid. Check log file for details.") | |
logger.warning(f"Waypoint file {filename} is invalid or old version. " | |
f"It does not contain a 'GlobalShoppingList' waypoint.") | |
s = None | |
# Check the | |
err = False | |
for key, value in s.items(): | |
if key == 'GlobalShoppingList': | |
# Special case | |
if 'BuyCommodities' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'BuyCommodities'.") | |
err = True | |
if 'UpdateCommodityCount' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'UpdateCommodityCount'.") | |
err = True | |
if 'Skip' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'Skip'.") | |
err = True | |
else: | |
# All other cases | |
if 'SystemName' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'SystemName'.") | |
err = True | |
if 'StationName' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'StationName'.") | |
err = True | |
if 'GalaxyBookmarkType' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'GalaxyBookmarkType'.") | |
err = True | |
if 'GalaxyBookmarkNumber' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'GalaxyBookmarkNumber'.") | |
err = True | |
if 'SystemBookmarkType' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'SystemBookmarkType'.") | |
err = True | |
if 'SystemBookmarkNumber' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'SystemBookmarkNumber'.") | |
err = True | |
if 'SellCommodities' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'SellCommodities'.") | |
err = True | |
if 'BuyCommodities' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'BuyCommodities'.") | |
err = True | |
if 'UpdateCommodityCount' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'UpdateCommodityCount'.") | |
err = True | |
if 'FleetCarrierTransfer' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'FleetCarrierTransfer'.") | |
err = True | |
if 'Skip' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'Skip'.") | |
err = True | |
if 'Completed' not in value: | |
logger.warning(f"Waypoint file key '{key}' does not contain 'Completed'.") | |
err = True | |
if err: | |
s = None | |
except Exception as e: | |
logger.warning("EDWayPoint.py read_waypoints error :" + str(e)) | |
return s | |
def write_waypoints(self, data, filename='./waypoints/waypoints.json'): | |
if data is None: | |
data = self.waypoints | |
try: | |
with open(filename, "w") as fp: | |
json.dump(data, fp, indent=4) | |
except Exception as e: | |
logger.warning("EDWayPoint.py write_waypoints error:" + str(e)) | |
def mark_waypoint_complete(self, key): | |
self.waypoints[key]['Completed'] = True | |
self.write_waypoints(data=None, filename='./waypoints/' + Path(self.filename).name) | |
def get_waypoint(self) -> tuple[str, dict] | tuple[None, None]: | |
""" Returns the next waypoint list or None if we are at the end of the waypoints. | |
""" | |
dest_key = "-1" | |
# loop back to beginning if last record is "REPEAT" | |
while dest_key == "-1": | |
for i, key in enumerate(self.waypoints): | |
# skip records we already processed | |
if i < self.step: | |
continue | |
# if this entry is REPEAT (and not skipped), mark them all as Completed = False | |
if ((self.waypoints[key].get('SystemName', "").upper() == "REPEAT") | |
and not self.waypoints[key]['Skip']): | |
self.mark_all_waypoints_not_complete() | |
break | |
# if this step is marked to skip... i.e. completed, go to next step | |
if (key == "GlobalShoppingList" or self.waypoints[key]['Completed'] | |
or self.waypoints[key]['Skip']): | |
continue | |
# This is the next uncompleted step | |
self.step = i | |
dest_key = key | |
break | |
else: | |
return None, None | |
return dest_key, self.waypoints[dest_key] | |
def mark_all_waypoints_not_complete(self): | |
for j, tkey in enumerate(self.waypoints): | |
# Ensure 'Completed' key exists before trying to set it | |
if 'Completed' in self.waypoints[tkey]: | |
self.waypoints[tkey]['Completed'] = False | |
else: | |
# Handle legacy format where 'Completed' might be missing | |
# Or log a warning if the structure is unexpected | |
logger.warning(f"Waypoint {tkey} missing 'Completed' key during reset.") | |
self.step = 0 | |
self.write_waypoints(data=None, filename='./waypoints/' + Path(self.filename).name) | |
self.log_stats() | |
def is_station_targeted(self, dest_key) -> bool: | |
""" Check if a station is specified in the waypoint by name or by bookmark.""" | |
if self.waypoints[dest_key]['StationName'] is not None: | |
if self.waypoints[dest_key]['StationName'] != "": | |
return True | |
if self.waypoints[dest_key]['SystemBookmarkNumber'] is not None: | |
if self.waypoints[dest_key]['SystemBookmarkNumber'] != -1: | |
return True | |
return False | |
def log_stats(self): | |
calc1 = 1.5 ** self.stats_log['Colonisation'] | |
calc2 = 1.5 ** self.stats_log['Construction'] | |
sleep(max(calc1, calc2)) | |
def execute_trade(self, ap, dest_key): | |
# Get trade commodities from waypoint | |
sell_commodities = self.waypoints[dest_key]['SellCommodities'] | |
buy_commodities = self.waypoints[dest_key]['BuyCommodities'] | |
fleetcarrier_transfer = self.waypoints[dest_key]['FleetCarrierTransfer'] | |
global_buy_commodities = self.waypoints['GlobalShoppingList']['BuyCommodities'] | |
if len(sell_commodities) == 0 and len(buy_commodities) == 0 and len(global_buy_commodities) == 0: | |
return | |
# Does this place have commodities service? | |
# From the journal, this works for stations (incl. outpost), colonisation ship and megaships | |
if ap.jn.ship_state()['StationServices'] is not None: | |
if 'commodities' not in ap.jn.ship_state()['StationServices']: | |
self.ap.ap_ckb('log', f"No commodities market at docked location.") | |
return | |
else: | |
self.ap.ap_ckb('log', f"No station services at docked location.") | |
return | |
# Determine type of station we are at | |
colonisation_ship = "ColonisationShip".upper() in ap.jn.ship_state()['cur_station'].upper() | |
orbital_construction_site = ap.jn.ship_state()['cur_station_type'].upper() == "SpaceConstructionDepot".upper() | |
fleet_carrier = ap.jn.ship_state()['cur_station_type'].upper() == "FleetCarrier".upper() | |
outpost = ap.jn.ship_state()['cur_station_type'].upper() == "Outpost".upper() | |
if colonisation_ship or orbital_construction_site: | |
if colonisation_ship: | |
# Colonisation Ship | |
self.stats_log['Colonisation'] = self.stats_log['Colonisation'] + 1 | |
self.ap.ap_ckb('log', f"Executing trade with Colonisation Ship.") | |
logger.debug(f"Execute Trade: On Colonisation Ship") | |
if orbital_construction_site: | |
# Construction Ship | |
self.stats_log['Construction'] = self.stats_log['Construction'] + 1 | |
self.ap.ap_ckb('log', f"Executing trade with Orbital Construction Ship.") | |
logger.debug(f"Execute Trade: On Orbital Construction Site") | |
# Go to station services | |
self.ap.stn_svcs_in_ship.goto_station_services() | |
# --------- SELL ---------- | |
if len(sell_commodities) > 0: | |
# Sell all to colonisation/construction ship | |
self.sell_to_colonisation_ship(ap) | |
elif fleet_carrier and fleetcarrier_transfer: | |
# Fleet Carrier in Transfer mode | |
self.stats_log['Fleet Carrier'] = self.stats_log['Fleet Carrier'] + 1 | |
# --------- SELL ---------- | |
if len(sell_commodities) > 0: | |
# Transfer to Fleet Carrier | |
self.ap.internal_panel.transfer_to_fleetcarrier(ap) | |
# --------- BUY ---------- | |
if len(buy_commodities) > 0: | |
self.ap.internal_panel.transfer_from_fleetcarrier(ap, buy_commodities) | |
else: | |
# Regular Station or Fleet Carrier in Buy/Sell mode | |
self.ap.ap_ckb('log', "Executing trade.") | |
logger.debug(f"Execute Trade: On Regular Station") | |
self.stats_log['Station'] = self.stats_log['Station'] + 1 | |
self.market_parser.get_market_data() | |
market_time_old = self.market_parser.current_data['timestamp'] | |
# We start off on the Main Menu in the Station | |
self.ap.stn_svcs_in_ship.goto_station_services() | |
# CONNECTED TO menu is different between stations and fleet carriers | |
if fleet_carrier: | |
# Fleet Carrier COMMODITIES MARKET location top right, with: | |
# uni cart, redemption, trit depot, shipyard, crew lounge | |
ap.keys.send('UI_Right', repeat=2) | |
ap.keys.send('UI_Select') # Select Commodities | |
elif outpost: | |
# Outpost COMMODITIES MARKET location in middle column | |
ap.keys.send('UI_Right') | |
ap.keys.send('UI_Select') # Select Commodities | |
else: | |
# Orbital station COMMODITIES MARKET location bottom left | |
ap.keys.send('UI_Down') | |
ap.keys.send('UI_Select') # Select Commodities | |
self.ap.ap_ckb('log+vce', "Downloading commodities data from market.") | |
# Wait for market to update | |
self.market_parser.get_market_data() | |
market_time_new = self.market_parser.current_data['timestamp'] | |
while market_time_new == market_time_old: | |
self.market_parser.get_market_data() | |
market_time_new = self.market_parser.current_data['timestamp'] | |
sleep(1) # wait for new menu to finish rendering | |
cargo_capacity = ap.jn.ship_state()['cargo_capacity'] | |
logger.info(f"Execute trade: Current cargo capacity: {cargo_capacity}") | |
# --------- SELL ---------- | |
if len(sell_commodities) > 0: | |
# Select the SELL option | |
self.ap.stn_svcs_in_ship.select_sell(ap.keys) | |
for i, key in enumerate(sell_commodities): | |
# Check if we have any of the item to sell | |
self.cargo_parser.get_cargo_data() | |
cargo_item = self.cargo_parser.get_item(key) | |
if cargo_item is None: | |
logger.info(f"Unable to sell {key}. None in cargo hold.") | |
continue | |
# Sell the commodity | |
result, qty = self.ap.stn_svcs_in_ship.sell_commodity(ap.keys, key, sell_commodities[key], | |
self.cargo_parser) | |
# Update counts if necessary | |
if qty > 0 and self.waypoints[dest_key]['UpdateCommodityCount']: | |
sell_commodities[key] = sell_commodities[key] - qty | |
# Save changes | |
self.write_waypoints(data=None, filename='./waypoints/' + Path(self.filename).name) | |
sleep(1) | |
# --------- BUY ---------- | |
if len(buy_commodities) > 0 or len(global_buy_commodities) > 0: | |
# Select the BUY option | |
self.ap.stn_svcs_in_ship.select_buy(ap.keys) | |
# Go through buy commodities list | |
for i, key in enumerate(buy_commodities): | |
curr_cargo_qty = int(ap.status.get_cleaned_data()['Cargo']) | |
cargo_timestamp = ap.status.current_data['timestamp'] | |
free_cargo = cargo_capacity - curr_cargo_qty | |
logger.info(f"Execute trade: Free cargo space: {free_cargo}") | |
if free_cargo == 0: | |
logger.info(f"Execute trade: No space for additional cargo") | |
break | |
qty_to_buy = buy_commodities[key] | |
logger.info(f"Execute trade: Shopping list requests {qty_to_buy} units of {key}") | |
# Attempt to buy the commodity | |
result, qty = self.ap.stn_svcs_in_ship.buy_commodity(ap.keys, key, qty_to_buy, free_cargo) | |
logger.info(f"Execute trade: Bought {qty} units of {key}") | |
# If we bought any goods, wait for status file to update with | |
# new cargo count for next commodity | |
if qty > 0: | |
ap.status.wait_for_file_change(cargo_timestamp, 5) | |
# Update counts if necessary | |
if qty > 0 and self.waypoints[dest_key]['UpdateCommodityCount']: | |
buy_commodities[key] = qty_to_buy - qty | |
# Go through global buy commodities list | |
for i, key in enumerate(global_buy_commodities): | |
curr_cargo_qty = int(ap.status.get_cleaned_data()['Cargo']) | |
cargo_timestamp = ap.status.current_data['timestamp'] | |
free_cargo = cargo_capacity - curr_cargo_qty | |
logger.info(f"Execute trade: Free cargo space: {free_cargo}") | |
if free_cargo == 0: | |
logger.info(f"Execute trade: No space for additional cargo") | |
break | |
qty_to_buy = global_buy_commodities[key] | |
logger.info(f"Execute trade: Global shopping list requests {qty_to_buy} units of {key}") | |
# Attempt to buy the commodity | |
result, qty = self.ap.stn_svcs_in_ship.buy_commodity(ap.keys, key, qty_to_buy, free_cargo) | |
logger.info(f"Execute trade: Bought {qty} units of {key}") | |
# If we bought any goods, wait for status file to update with | |
# new cargo count for next commodity | |
if qty > 0: | |
ap.status.wait_for_file_change(cargo_timestamp, 5) | |
# Update counts if necessary | |
if qty > 0 and self.waypoints['GlobalShoppingList']['UpdateCommodityCount']: | |
global_buy_commodities[key] = qty_to_buy - qty | |
# Save changes | |
self.write_waypoints(data=None, filename='./waypoints/' + Path(self.filename).name) | |
sleep(1.5) # give time to popdown | |
# Go to ship view | |
ap.ship_control.goto_cockpit_view() | |
def sell_to_colonisation_ship(self, ap): | |
""" Sell all cargo to a colonisation/construction ship. | |
""" | |
ap.keys.send('UI_Left', repeat=3) # Go to table | |
ap.keys.send('UI_Down', hold=2) # Go to bottom | |
ap.keys.send('UI_Up') # Select RESET/CONFIRM TRANSFER/TRANSFER ALL | |
ap.keys.send('UI_Left', repeat=2) # Go to RESET | |
ap.keys.send('UI_Right', repeat=2) # Go to TRANSFER ALL | |
ap.keys.send('UI_Select') # Select TRANSFER ALL | |
sleep(0.5) | |
ap.keys.send('UI_Left') # Go to CONFIRM TRANSFER | |
ap.keys.send('UI_Select') # Select CONFIRM TRANSFER | |
sleep(2) | |
ap.keys.send('UI_Down') # Go to EXIT | |
ap.keys.send('UI_Select') # Select EXIT | |
sleep(2) # give time to popdown menu | |
def waypoint_assist(self, keys, scr_reg): | |
""" Processes the waypoints, performing jumps and sc assist if going to a station | |
also can then perform trades if specific in the waypoints file. | |
""" | |
if len(self.waypoints) == 0: | |
self.ap.ap_ckb('log+vce', "No Waypoint file loaded. Exiting Waypoint Assist.") | |
return | |
self.step = 0 # start at first waypoint | |
self.ap.ap_ckb('log', "Waypoint file: " + str(Path(self.filename).name)) | |
self.reset_stats() | |
# Loop until complete, or error | |
_abort = False | |
while not _abort: | |
# Current location | |
cur_star_system = self.ap.jn.ship_state()['cur_star_system'].upper() | |
cur_station = self.ap.jn.ship_state()['cur_station'].upper() | |
cur_station_type = self.ap.jn.ship_state()['cur_station_type'].upper() | |
# Current in game destination | |
status = self.ap.status.get_cleaned_data() | |
destination_system = status['Destination_System'] # The system ID | |
destination_body = status['Destination_Body'] # The body number (0 for prim star) | |
destination_name = status['Destination_Name'] # The system/body/station/settlement name | |
# ==================================== | |
# Get next Waypoint | |
# ==================================== | |
# Get the waypoint details | |
old_step = self.step | |
dest_key, next_waypoint = self.get_waypoint() | |
if dest_key is None: | |
self.ap.ap_ckb('log+vce', "Waypoint list has been completed.") | |
break | |
# Is this a new waypoint? | |
if self.step != old_step: | |
new_waypoint = True | |
else: | |
new_waypoint = False | |
# Flag if we are using bookmarks | |
gal_bookmark = next_waypoint.get('GalaxyBookmarkNumber', -1) > 0 | |
sys_bookmark = next_waypoint.get('SystemBookmarkNumber', -1) > 0 | |
gal_bookmark_type = next_waypoint.get('GalaxyBookmarkType', '') | |
gal_bookmark_num = next_waypoint.get('GalaxyBookmarkNumber', 0) | |
sys_bookmark_type = next_waypoint.get('SystemBookmarkType', '') | |
sys_bookmark_num = next_waypoint.get('SystemBookmarkNumber', 0) | |
next_wp_system = next_waypoint.get('SystemName', '').upper() | |
next_wp_station = next_waypoint.get('StationName', '').upper() | |
if new_waypoint: | |
self.ap.ap_ckb('log+vce', f"Next Waypoint: {next_wp_station} in {next_wp_system}") | |
# ==================================== | |
# Target and travel to a System | |
# ==================================== | |
# Check current system and go to next system if different and not blank | |
if next_wp_system == "" or (cur_star_system == next_wp_system): | |
if new_waypoint: | |
self.ap.ap_ckb('log+vce', f"Already in target System.") | |
else: | |
# Check if the current nav route is to the target system | |
last_nav_route_sys = self.ap.nav_route.get_last_system().upper() | |
# Check we have a route and that we have a destination to a star (body 0). | |
# We can have one without the other. | |
if ((last_nav_route_sys == next_wp_system) and | |
(destination_body == 0 and destination_name != "")): | |
# No need to target system | |
self.ap.ap_ckb('log+vce', f"System already targeted.") | |
else: | |
self.ap.ap_ckb('log+vce', f"Targeting system {next_wp_system}.") | |
# Select destination in galaxy map based on name | |
res = self.ap.galaxy_map.set_gal_map_destination_text(self.ap, next_wp_system, | |
self.ap.jn.ship_state) | |
if res: | |
self.ap.ap_ckb('log', f"System has been targeted.") | |
else: | |
self.ap.ap_ckb('log+vce', f"Unable to target {next_wp_system} in Galaxy Map.") | |
_abort = True | |
break | |
# Select next target system | |
# TODO should this be in before every jump? | |
keys.send('TargetNextRouteSystem') | |
# Jump to the system | |
self.ap.ap_ckb('log+vce', f"Jumping to {next_wp_system}.") | |
res = self.ap.jump_to_system(scr_reg, next_wp_system) | |
if not res: | |
self.ap.ap_ckb('log', f"Failed to jump to {next_wp_system}.") | |
_abort = True | |
break | |
continue | |
# ==================================== | |
# Target and travel to a local Station | |
# ==================================== | |
# If we are in the right system, check if we are already docked. | |
docked_at_stn = False | |
is_docked = self.ap.status.get_flag(FlagsDocked) | |
if is_docked: | |
# Check if we are at the correct station. Note that for FCs, the station name | |
# reported by the Journal is only the ship identifier (ABC-123) and not the carrier name. | |
# So we need to check if the ID (ABC-123) is at the end of the target ('Fleety McFleet ABC-123'). | |
if cur_station_type == 'FleetCarrier'.upper(): | |
docked_at_stn = next_wp_station.endswith(cur_station) | |
elif next_wp_station == 'System Colonisation Ship'.upper(): | |
if (cur_station_type == 'SurfaceStation'.upper() and | |
'ColonisationShip'.upper() in cur_station.upper()): | |
docked_at_stn = True | |
# elif next_wp_station.startswith('Orbital Construction Site'.upper()): | |
# if (cur_station_type == 'SurfaceStation'.upper() and | |
# 'Orbital Construction Site'.upper() in cur_station.upper()): | |
# docked_at_stn = True | |
elif cur_station == next_wp_station: | |
docked_at_stn = True | |
# Check current station and go to it if different | |
if docked_at_stn: | |
if new_waypoint: | |
self.ap.ap_ckb('log+vce', f"Already at target Station: {next_wp_station}") | |
else: | |
# Check if we need to travel to a station, else we are done. | |
# This may be by 1) System bookmark, 2) Galaxy bookmark or 3) by Station Name text | |
if sys_bookmark or gal_bookmark or next_wp_station != "": | |
# If waypoint file has a Station Name associated then attempt targeting it | |
self.ap.ap_ckb('log+vce', f"Targeting Station: {next_wp_station}") | |
if gal_bookmark: | |
# Set destination via gal bookmark, not system bookmark | |
res = self.ap.galaxy_map.set_gal_map_dest_bookmark(self.ap, gal_bookmark_type, gal_bookmark_num) | |
if not res: | |
self.ap.ap_ckb('log+vce', f"Unable to set Galaxy Map bookmark.") | |
_abort = True | |
break | |
elif sys_bookmark: | |
# Set destination via system bookmark | |
res = self.ap.system_map.set_sys_map_dest_bookmark(self.ap, sys_bookmark_type, sys_bookmark_num) | |
if not res: | |
self.ap.ap_ckb('log+vce', f"Unable to set System Map bookmark.") | |
_abort = True | |
break | |
elif next_wp_station != "": | |
# Need OCR added in for this (WIP) | |
need_ocr = True | |
self.ap.ap_ckb('log+vce', f"No bookmark defined. Target by Station text not supported.") | |
# res = self.nav_panel.lock_destination(station_name) | |
_abort = True | |
break | |
# Jump to the station by name | |
res = self.ap.supercruise_to_station(scr_reg, next_wp_station) | |
sleep(1) # Allow status log to update | |
continue | |
else: | |
self.ap.ap_ckb('log+vce', f"Arrived at target System: {next_wp_system}") | |
# ==================================== | |
# Dock and Trade at Station | |
# ==================================== | |
# Are we at the correct station to trade? | |
if docked_at_stn: # and (next_wp_station != "" or sys_bookmark): | |
# Docked - let do trade | |
self.ap.ap_ckb('log+vce', f"Execute trade at Station: {next_wp_station}") | |
self.execute_trade(self.ap, dest_key) | |
# Mark this waypoint as completed | |
self.mark_waypoint_complete(dest_key) | |
self.ap.ap_ckb('log+vce', f"Current Waypoint complete.") | |
# Done with waypoints | |
if not _abort: | |
self.ap.ap_ckb('log+vce', | |
"Waypoint Route Complete, total distance jumped: " + str(self.ap.total_dist_jumped) + "LY") | |
self.ap.update_ap_status("Idle") | |
else: | |
self.ap.ap_ckb('log+vce', "Waypoint Route was aborted.") | |
self.ap.update_ap_status("Idle") | |
def reset_stats(self): | |
# Clear stats | |
self.stats_log['Colonisation'] = 0 | |
self.stats_log['Construction'] = 0 | |
self.stats_log['Fleet Carrier'] = 0 | |
self.stats_log['Station'] = 0 | |
def main(): | |
from ED_AP import EDAutopilot | |
ed_ap = EDAutopilot(cb=None) | |
wp = EDWayPoint(ed_ap, True) # False = Horizons | |
wp.step = 0 # start at first waypoint | |
keys = EDKeys(cb=None) | |
keys.activate_window = True | |
wp.ap.stn_svcs_in_ship.select_sell(keys) | |
wp.ap.stn_svcs_in_ship.sell_commodity(keys, "Aluminium", 1, wp.cargo_parser) | |
wp.ap.stn_svcs_in_ship.sell_commodity(keys, "Beryllium", 1, wp.cargo_parser) | |
wp.ap.stn_svcs_in_ship.sell_commodity(keys, "Cobalt", 1, wp.cargo_parser) | |
#wp.ap.stn_svcs_in_ship.buy_commodity(keys, "Titanium", 5, 200) | |
# dest = 'Enayex' | |
#print(dest) | |
#print("In waypoint_assist, at:"+str(dest)) | |
# already in doc config, test the trade | |
#wp.execute_trade(keys, dest) | |
# Set the Route for the waypoint^# | |
#dest = wp.waypoint_next(ap=None) | |
#while dest != "": | |
# print("Doing: "+str(dest)) | |
# print(wp.waypoints[dest]) | |
# print("Dock w/station: "+ str(wp.is_station_targeted(dest))) | |
#wp.set_station_target(None, dest) | |
# Mark this waypoint as complated | |
#wp.mark_waypoint_complete(dest) | |
# set target to next waypoint and loop | |
#dest = wp.waypoint_next(ap=None) | |
if __name__ == "__main__": | |
main() | |
--- | |
File: /Image_Templates.py | |
--- | |
import sys | |
from os.path import abspath, getmtime, isfile, join, dirname | |
import cv2 | |
from EDlogger import logger | |
""" | |
File:Image_Templates.py | |
Description: | |
Class defines template images that will be used with opencv to match in screen regions | |
Author: [email protected] | |
""" | |
class Image_Templates: | |
def __init__(self, scaleX, scaleY, compass_scale: float): | |
self.template = { 'elw' : {'image': None, 'width': 1, 'height': 1}, | |
'elw_sig' : {'image': None, 'width': 1, 'height': 1}, | |
'navpoint' : {'image': None, 'width': 1, 'height': 1}, | |
'navpoint-behind': {'image': None, 'width': 1, 'height': 1}, | |
'compass' : {'image': None, 'width': 1, 'height': 1}, | |
'target' : {'image': None, 'width': 1, 'height': 1}, | |
'target_occluded' : {'image': None, 'width': 1, 'height': 1}, | |
'disengage' : {'image': None, 'width': 1, 'height': 1}, | |
'missions' : {'image': None, 'width': 1, 'height': 1}, | |
'dest_sirius' : {'image': None, 'width': 1, 'height': 1}, | |
'sirius_atmos' : {'image': None, 'width': 1, 'height': 1} | |
} | |
# load the templates and scale them. Default templates assumed 3440x1440 screen resolution | |
self.reload_templates(scaleX, scaleY, compass_scale) | |
def load_template(self, file_name, scaleX, scaleY): | |
""" Load the template image in color. If we need grey scale for matching, we can apply that later as needed. | |
Resize the image, as the templates are based on 3440x1440 resolution, so scale to current screen resolution | |
return image and size info. """ | |
template = cv2.imread(self.resource_path(file_name), cv2.IMREAD_GRAYSCALE) | |
#logger.debug("File:"+self.resource_path(file_name)+" template:"+str(template)) | |
template = cv2.resize(template, (0, 0), fx=scaleX, fy=scaleY) | |
width, height = template.shape[::-1] | |
return {'image': template, 'width': width, 'height': height} | |
def reload_templates(self, scaleX, scaleY, compass_scale: float): | |
""" Load the full set of image templates. """ | |
self.template['elw'] = self.load_template("templates/elw-template.png", scaleX, scaleY) | |
self.template['elw_sig'] = self.load_template("templates/elw-sig-template.png", scaleX, scaleY) | |
self.template['navpoint'] = self.load_template("templates/navpoint.png", compass_scale, compass_scale) | |
self.template['navpoint-behind'] = self.load_template("templates/navpoint-behind.png", compass_scale, compass_scale) | |
self.template['compass'] = self.load_template("templates/compass.png", compass_scale,compass_scale) | |
self.template['target'] = self.load_template("templates/destination.png", scaleX, scaleY) | |
self.template['target_occluded'] = self.load_template("templates/target_occluded.png", scaleX, scaleY) | |
self.template['disengage'] = self.load_template("templates/sc-disengage.png", scaleX, scaleY) | |
self.template['missions'] = self.load_template("templates/completed-missions.png", scaleX, scaleY) | |
self.template['dest_sirius'] = self.load_template("templates/dest-sirius-atmos-HL.png", scaleX, scaleY) | |
self.template['robigo_mines'] = self.load_template("templates/robigo-mines-selected.png", scaleX, scaleY) | |
self.template['sirius_atmos'] = self.load_template("templates/sirius-atmos-selected.png", scaleX, scaleY) | |
def resource_path(self,relative_path): | |
""" Get absolute path to resource, works for dev and for PyInstaller """ | |
#try: | |
# PyInstaller creates a temp folder and stores path in _MEIPASS | |
# base_path = sys._MEIPASS | |
#except Exception: | |
#base_path = abspath(".") | |
base_path = abspath(".") | |
return join(base_path, relative_path) | |
--- | |
File: /MarketParser.py | |
--- | |
from __future__ import annotations | |
import json | |
import os | |
import time | |
from datetime import datetime, timedelta | |
import queue | |
from operator import itemgetter | |
from sys import platform | |
import threading | |
from time import sleep | |
from EDlogger import logger | |
from WindowsKnownPaths import * | |
class MarketParser: | |
""" Parses the Market.json file generated by the game. """ | |
def __init__(self, file_path=None): | |
if platform != "win32": | |
self.file_path = file_path if file_path else "./linux_ed/Market.json" | |
else: | |
from WindowsKnownPaths import get_path, FOLDERID, UserHandle | |
self.file_path = file_path if file_path else (get_path(FOLDERID.SavedGames, UserHandle.current) | |
+ "/Frontier Developments/Elite Dangerous/Market.json") | |
self.last_mod_time = None | |
# Read json file data | |
self.current_data = self.get_market_data() | |
# self.watch_thread = threading.Thread(target=self._watch_file_thread, daemon=True) | |
# self.watch_thread.start() | |
# self.status_queue = queue.Queue() | |
# def _watch_file_thread(self): | |
# backoff = 1 | |
# while True: | |
# try: | |
# self._watch_file() | |
# except Exception as e: | |
# logger.debug('An error occurred when reading status file') | |
# sleep(backoff) | |
# logger.debug('Attempting to restart status file reader after failure') | |
# backoff *= 2 | |
# | |
# def _watch_file(self): | |
# """Detects changes in the Status.json file.""" | |
# while True: | |
# status = self.get_cleaned_data() | |
# if status != self.current_data: | |
# self.status_queue.put(status) | |
# self.current_data = status | |
# sleep(1) | |
def get_file_modified_time(self) -> float: | |
return os.path.getmtime(self.file_path) | |
def get_market_data(self): | |
"""Loads data from the JSON file and returns the data. | |
{ | |
"timestamp": "2024-09-21T14:53:38Z", | |
"event": "Market", | |
"MarketID": 129019775, | |
"StationName": "Rescue Ship Cornwallis", | |
"StationType": "MegaShip", | |
"StarSystem": "V886 Centauri", | |
"Items": [ | |
{ | |
"id": 128049152, | |
"Name": "$platinum_name;", | |
"Name_Localised": "Platinum", | |
"Category": "$MARKET_category_metals;", | |
"Category_Localised": "Metals", | |
"BuyPrice": 3485, | |
"SellPrice": 3450, | |
"MeanPrice": 58272, | |
"StockBracket": 0, | |
"DemandBracket": 0, | |
"Stock": 0, | |
"Demand": 0, | |
"Consumer": false, | |
"Producer": false, | |
"Rare": false | |
}, { etc. } ] | |
""" | |
# Check if file changed | |
if self.get_file_modified_time() == self.last_mod_time: | |
#logger.debug(f'Market.json mod timestamp {self.last_mod_time} unchanged.') | |
return self.current_data | |
# Read file | |
backoff = 1 | |
while True: | |
try: | |
with open(self.file_path, 'r') as file: | |
data = json.load(file) | |
break | |
except Exception as e: | |
logger.debug('An error occurred reading Market.json file. File may be open.') | |
sleep(backoff) | |
logger.debug('Attempting to re-read Market.json file after delay.') | |
backoff *= 2 | |
# Store data | |
self.current_data = data | |
self.last_mod_time = self.get_file_modified_time() | |
#logger.debug(f'Market.json mod timestamp {self.last_mod_time} updated.') | |
# print(json.dumps(data, indent=4)) | |
return data | |
def get_sellable_items(self, cargo_parser) -> list: | |
""" Get a list of items that can be sold to the station. | |
Will trigger a read of the json file. | |
{ | |
"id": 128049154, | |
"Name": "$gold_name;", | |
"Name_Localised": "Gold", | |
"Category": "$MARKET_category_metals;", | |
"Category_Localised": "Metals", | |
"BuyPrice": 49118, | |
"SellPrice": 48558, | |
"MeanPrice": 47609, | |
"StockBracket": 2, | |
"DemandBracket": 0, | |
"Stock": 89, | |
"Demand": 1, | |
"Consumer": true, | |
"Producer": false, | |
"Rare": false | |
} | |
@param cargo_parser: Current cargo to check if rare or demand=1 items exist in hold. | |
@return: A list of commodities that can be sold. | |
""" | |
data = self.get_market_data() | |
sellable_items = [x for x in data['Items'] if x['Consumer'] or | |
((x['Demand'] > 1 or x['Demand']) and cargo_parser.get_item(x['Name_Localised']) is not None)] | |
# DemandBracket: 0=Not listed, 1=Low Demand, 2=Medium Demand, 3=High Demand | |
# sellable_items = [x for x in data['Items'] if x['DemandBracket'] > 0] | |
#sellable_items = [x for x in data['Items'] if self.can_sell_item(x['Name_Localised'])] | |
# print(json.dumps(newlist, indent=4)) | |
# Sort by name, then category | |
sorted_list1 = sorted(sellable_items, key=lambda x: x['Name_Localised'].lower()) | |
sorted_list2 = sorted(sorted_list1, key=lambda x: x['Category_Localised'].lower()) | |
logger.debug("Listing sellable commodities in market order") | |
for x in sorted_list2: | |
logger.debug(f"\t{x}") | |
logger.debug("Finished listing sellable commodities in market order") | |
return sorted_list2 | |
def get_buyable_items(self): | |
""" Get a list of items that can be bought from the station. | |
Will trigger a read of the json file. | |
{ | |
"id": 128049154, | |
"Name": "$gold_name;", | |
"Name_Localised": "Gold", | |
"Category": "$MARKET_category_metals;", | |
"Category_Localised": "Metals", | |
"BuyPrice": 49118, | |
"SellPrice": 48558, | |
"MeanPrice": 47609, | |
"StockBracket": 2, | |
"DemandBracket": 0, | |
"Stock": 89, | |
"Demand": 1, | |
"Consumer": false, | |
"Producer": true, | |
"Rare": false | |
} | |
""" | |
data = self.get_market_data() | |
# buyable_items = [x for x in data['Items'] if x['Producer'] and x['Stock'] > 0] | |
# StockBracket: 0=Not listed, 1=Low Stock, 2=Medium Stock, 3=High Stock | |
# buyable_items = [x for x in data['Items'] if x['StockBracket'] > 0] | |
buyable_items = [x for x in data['Items'] if self.can_buy_item(x['Name_Localised'])] | |
# print(json.dumps(newlist, indent=4)) | |
# Sort by name, then category | |
sorted_list1 = sorted(buyable_items, key=lambda x: x['Name_Localised'].lower()) | |
sorted_list2 = sorted(sorted_list1, key=lambda x: x['Category_Localised'].lower()) | |
logger.debug("Listing buyable commodities in market order") | |
for x in sorted_list2: | |
logger.debug(f"\t{x}") | |
logger.debug("Finished listing buyable commodities in market order") | |
return sorted_list2 | |
def get_market_name(self) -> str: | |
""" Gets the current market (station) name. | |
Will not trigger a read of the json file. | |
""" | |
return self.current_data['StationName'] | |
def get_item(self, item_name) -> dict[any] | None: | |
""" Get details of one item. Returns the item detail as below, or None if item does not exist. | |
Will not trigger a read of the json file. | |
{ | |
"id": 128049154, | |
"Name": "$gold_name;", | |
"Name_Localised": "Gold", | |
"Category": "$MARKET_category_metals;", | |
"Category_Localised": "Metals", | |
"BuyPrice": 49118, | |
"SellPrice": 48558, | |
"MeanPrice": 47609, | |
"StockBracket": 2, | |
"DemandBracket": 0, | |
"Stock": 89, | |
"Demand": 1, | |
"Consumer": false, | |
"Producer": true, | |
"Rare": false | |
} | |
""" | |
for good in self.current_data['Items']: | |
if good['Name_Localised'].upper() == item_name.upper(): | |
# print(json.dumps(good, indent=4)) | |
return good | |
return None | |
def can_buy_item(self, item_name: str) -> bool: | |
""" Can the item be bought from the market (is it sold and is there stock). | |
Will not trigger a read of the json file. | |
""" | |
good = self.get_item(item_name) | |
if good is None: | |
return False | |
return ((good['Stock'] > 0 and good['Producer'] and not good['Rare']) | |
or (good['Stock'] > 0 and good['Rare'])) # Need producer in for non-Rares? | |
def can_sell_item(self, item_name: str) -> bool: | |
""" Can the item be sold to the market (is it bought, regardless of demand). | |
Will not trigger a read of the json file. | |
""" | |
good = self.get_item(item_name) | |
if good is None: | |
return False | |
return good['Consumer'] or good['Demand'] > 0 or good['Rare'] | |
# Usage Example | |
if __name__ == "__main__": | |
parser = MarketParser() | |
while True: | |
cleaned_data = parser.get_market_data() | |
#item = parser.get_item('water') | |
#sell = parser.can_sell_item('water') | |
#buy = parser.can_buy_item('water') | |
items = parser.get_buyable_items() | |
#items = parser.get_sellable_items() | |
sorted_list = sorted(items, key=itemgetter('Name_Localised')) | |
sorted_list = sorted(sorted_list, key=itemgetter('Category_Localised')) | |
for item in sorted_list: | |
print(f"Item: {item['Name_Localised']} ({item['Category_Localised']}) DemandBracket:{item['DemandBracket']} StockBracket:{item['StockBracket']}") | |
#print(f"Curr Time: {time.time()}") | |
#print(f"Sell water: {sell}") | |
# print(json.dumps(cleaned_data, indent=4)) | |
time.sleep(1) | |
break | |
--- | |
File: /MousePt.py | |
--- | |
from pynput.mouse import * | |
from time import sleep | |
""" | |
File:MousePt.py | |
Description: | |
Class to handles getting x,y location for a mouse click, and a routine to click on a x, y location | |
Author: [email protected] | |
""" | |
class MousePoint: | |
def __init__(self): | |
self.x = 0 | |
self.y = 0 | |
self.term = False | |
self.ls = None # Listener(on_move=self.on_move, on_click=self.on_click, on_scroll=self.on_scroll) | |
self.ms = Controller() | |
def on_move(self, x, y): | |
return True | |
def on_scroll(self, x, y, dx, dy): | |
return True | |
def on_click(self, x, y, button, pressed): | |
self.x = x | |
self.y = y | |
self.term = True | |
return True | |
#print('{0} at {1}'.format('Pressed' if pressed else 'Released', (x1, y1))) | |
def get_location(self): | |
self.term = False | |
self.x = 0 | |
self.y = 0 | |
self.ls = Listener(on_move=self.on_move, on_click=self.on_click, on_scroll=self.on_scroll) | |
self.ls.start() | |
try: | |
while self.term == False: | |
sleep(0.5) | |
except: | |
pass | |
self.ls.stop() | |
return self.x, self.y | |
def do_click(self, x, y, delay = 0.1): | |
# position the mouse and do left click, duration in seconds | |
self.ms.position=(x, y) | |
#hself.ms.click(Button.left) | |
self.ms.press(Button.left) | |
sleep(delay) | |
self.ms.release(Button.left) | |
def main(): | |
m = MousePoint() | |
# x, y = m.get_location() | |
#print(str(x)+' '+str(y)) | |
# x, y = m.get_location() | |
# print(str(x)+' '+str(y)) | |
m.do_click(1977,510) | |
""" | |
for i in range(2): | |
m.do_click(1977,510) | |
sleep(0.5) | |
""" | |
if __name__ == "__main__": | |
main() | |
--- | |
File: /NavRouteParser.py | |
--- | |
from __future__ import annotations | |
import json | |
import os | |
import time | |
from sys import platform | |
from time import sleep | |
from EDlogger import logger | |
class NavRouteParser: | |
""" Parses the NavRoute.json file generated by the game. """ | |
def __init__(self, file_path=None): | |
if platform != "win32": | |
self.file_path = file_path if file_path else "./linux_ed/NavRoute.json" | |
else: | |
from WindowsKnownPaths import get_path, FOLDERID, UserHandle | |
self.file_path = file_path if file_path else (get_path(FOLDERID.SavedGames, UserHandle.current) | |
+ "/Frontier Developments/Elite Dangerous/NavRoute.json") | |
self.last_mod_time = None | |
# Read json file data | |
self.current_data = self.get_nav_route_data() | |
# self.watch_thread = threading.Thread(target=self._watch_file_thread, daemon=True) | |
# self.watch_thread.start() | |
# self.status_queue = queue.Queue() | |
# def _watch_file_thread(self): | |
# backoff = 1 | |
# while True: | |
# try: | |
# self._watch_file() | |
# except Exception as e: | |
# logger.debug('An error occurred when reading status file') | |
# sleep(backoff) | |
# logger.debug('Attempting to restart status file reader after failure') | |
# backoff *= 2 | |
# | |
# def _watch_file(self): | |
# """Detects changes in the Status.json file.""" | |
# while True: | |
# status = self.get_cleaned_data() | |
# if status != self.current_data: | |
# self.status_queue.put(status) | |
# self.current_data = status | |
# sleep(1) | |
def get_file_modified_time(self) -> float: | |
return os.path.getmtime(self.file_path) | |
def get_nav_route_data(self): | |
"""Loads data from the JSON file and returns the data. The first entry is the starting system. | |
When there is a route: | |
{ | |
"timestamp": "2024-09-29T20:02:20Z", "event": "NavRoute", "Route": [ | |
{"StarSystem": "Leesti", "SystemAddress": 3932277478114, "StarPos": [72.75000, 48.75000, 68.25000], | |
"StarClass": "K"}, | |
{"StarSystem": "Crucis Sector NY-R b4-3", "SystemAddress": 7268561200585, | |
"StarPos": [46.84375, 31.59375, 76.21875], "StarClass": "M"}, | |
{"StarSystem": "Devataru", "SystemAddress": 5069269509577, "StarPos": [24.28125, 19.34375, 90.93750], | |
"StarClass": "M"}, | |
{"StarSystem": "Scorpii Sector ZU-Y b4", "SystemAddress": 9467047519689, | |
"StarPos": [-0.50000, -0.65625, 86.65625], "StarClass": "M"}, | |
{"StarSystem": "HR 6836", "SystemAddress": 1384866908531, "StarPos": [-7.53125, -11.03125, 98.53125], | |
"StarClass": "F"} | |
]} | |
or... when route is clear: | |
{"timestamp": "2024-09-29T21:06:53Z", "event": "NavRouteClear", "Route": [ | |
]} | |
""" | |
# Check if file changed | |
if self.get_file_modified_time() == self.last_mod_time: | |
#logger.debug(f'NavRoute.json mod timestamp {self.last_mod_time} unchanged.') | |
return self.current_data | |
# Read file | |
backoff = 1 | |
while True: | |
try: | |
with open(self.file_path, 'r') as file: | |
data = json.load(file) | |
break | |
except Exception as e: | |
logger.debug('An error occurred reading NavRoute.json file. File may be open.') | |
sleep(backoff) | |
logger.debug('Attempting to re-read NavRoute.json file after delay.') | |
backoff *= 2 | |
# Store data | |
self.current_data = data | |
self.last_mod_time = self.get_file_modified_time() | |
#logger.debug(f'NavRoute.json mod timestamp {self.last_mod_time} updated.') | |
# print(json.dumps(data, indent=4)) | |
return data | |
def get_last_system(self) -> str: | |
""" Gets the final destination (system name) or empty string. | |
""" | |
# Get latest data | |
self.get_nav_route_data() | |
# Check if there is a route | |
if self.current_data['event'] == "NavRouteClear": | |
return '' | |
if self.current_data['Route'] is None: | |
return '' | |
# Find last system in route | |
last_system = self.current_data['Route'][-1] | |
# print(json.dumps(last_system, indent=4)) | |
return last_system['StarSystem'] | |
# Usage Example | |
if __name__ == "__main__": | |
parser = NavRouteParser() | |
while True: | |
item = parser.get_last_system() | |
print(f"last_system: {item}") | |
time.sleep(1) | |
--- | |
File: /OCR.py | |
--- | |
from __future__ import annotations | |
import time | |
import cv2 | |
import numpy as np | |
from paddleocr import PaddleOCR | |
from strsimpy import SorensenDice | |
from strsimpy.jaro_winkler import JaroWinkler | |
from EDlogger import logger | |
""" | |
File:OCR.py | |
Description: | |
Class for OCR processing using PaddleOCR. | |
Author: Stumpii | |
""" | |
class OCR: | |
def __init__(self, screen): | |
self.screen = screen | |
self.paddleocr = PaddleOCR(use_angle_cls=True, lang='en', use_gpu=False, show_log=False, use_dilation=True, | |
use_space_char=True) | |
# Class for text similarity metrics | |
self.jarowinkler = JaroWinkler() | |
self.sorensendice = SorensenDice() | |
def string_similarity(self, s1, s2): | |
#return self.jarowinkler.similarity(s1, s2) | |
return self.sorensendice.similarity(s1, s2) | |
def image_ocr(self, image): | |
""" Perform OCR with no filtering. Returns the full OCR data and a simplified list of strings. | |
This routine is the slower than the simplified OCR. | |
OCR Data is returned in the following format, or (None, None): | |
[[[[[86.0, 8.0], [208.0, 8.0], [208.0, 34.0], [86.0, 34.0]], ('ROBIGO 1 A', 0.9815958738327026)]]] | |
""" | |
ocr_data = self.paddleocr.ocr(image) | |
# print(ocr_data) | |
if ocr_data is None: | |
return None, None | |
else: | |
ocr_textlist = [] | |
for res in ocr_data: | |
if res is None: | |
return None, None | |
for line in res: | |
ocr_textlist.append(line[1][0]) | |
#print(ocr_textlist) | |
return ocr_data, ocr_textlist | |
def image_simple_ocr(self, image) -> list[str] | None: | |
""" Perform OCR with no filtering. Returns a simplified list of strings with no positional data. | |
This routine is faster than the function that returns the full data. Generally good when you | |
expect to only return one or two lines of text. | |
OCR Data is returned in the following format, or None: | |
[[[[[86.0, 8.0], [208.0, 8.0], [208.0, 34.0], [86.0, 34.0]], ('ROBIGO 1 A', 0.9815958738327026)]]] | |
""" | |
ocr_data = self.paddleocr.ocr(image) | |
# print(f"image_simple_ocr: {ocr_data}") | |
if ocr_data is None: | |
return None | |
else: | |
ocr_textlist = [] | |
for res in ocr_data: | |
if res is None: | |
return None | |
for line in res: | |
ocr_textlist.append(line[1][0]) | |
# print(f"image_simple_ocr: {ocr_textlist}") | |
# logger.info(f"image_simple_ocr: {ocr_textlist}") | |
return ocr_textlist | |
def get_highlighted_item_data(self, image, min_w, min_h): | |
""" Attempts to find a selected item in an image. The selected item is identified by being solid orange or blue | |
rectangle with dark text, instead of orange/blue text on a dark background. | |
The OCR daya of the first item matching the criteria is returned, otherwise None. | |
@param image: The image to check. | |
@param min_w: The minimum width of the text block. | |
@param min_h: The minimum height of the text block. | |
""" | |
# Find the selected item/menu (solid orange) | |
img_selected, x, y = self.get_highlighted_item_in_image(image, min_w, min_h) | |
if img_selected is not None: | |
# cv2.imshow("img", img_selected) | |
ocr_data, ocr_textlist = self.image_ocr(img_selected) | |
if ocr_data is not None: | |
return img_selected, ocr_data, ocr_textlist | |
else: | |
return None, None, None | |
else: | |
return None, None, None | |
def get_highlighted_item_in_image(self, image, min_w, min_h): | |
""" Attempts to find a selected item in an image. The selected item is identified by being solid orange or blue | |
rectangle with dark text, instead of orange/blue text on a dark background. | |
The image of the first item matching the criteria and minimum width and height is returned | |
with x and y co-ordinates, otherwise None. | |
@param image: The image to check. | |
@param min_h: Minimum height in pixels. | |
@param min_w: Minimum width in pixels. | |
""" | |
# Perform HSV mask | |
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) | |
lower_range = np.array([0, 100, 180]) | |
upper_range = np.array([255, 255, 255]) | |
mask = cv2.inRange(hsv, lower_range, upper_range) | |
masked_image = cv2.bitwise_and(image, image, mask=mask) | |
cv2.imwrite('test/nav-panel/out/masked.png', masked_image) | |
# Convert to gray scale and invert | |
gray = cv2.cvtColor(masked_image, cv2.COLOR_BGR2GRAY) | |
cv2.imwrite('test/nav-panel/out/gray.png', gray) | |
# Convert to B&W to allow FindContours to find rectangles. | |
ret, thresh1 = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU) # | cv2.THRESH_BINARY_INV) | |
cv2.imwrite('test/nav-panel/out/thresh1.png', thresh1) | |
# Finding contours in B&W image. White are the areas detected | |
contours, hierarchy = cv2.findContours(thresh1, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
cropped = image | |
for cnt in contours: | |
x, y, w, h = cv2.boundingRect(cnt) | |
# Check the item is greater than 90% of the minimum width or height. Which allows for some variation. | |
if w > (min_w * 0.9) and h > (min_h * 0.9): | |
# Drawing a rectangle on the copied image | |
# rect = cv2.rectangle(crop, (x, y), (x + w, y + h), (0, 255, 0), 2) | |
# Crop to leave only the contour (the selected rectangle) | |
cropped = image[y:y + h, x:x + w] | |
# cv2.imshow("cropped", cropped) | |
# cv2.imwrite('test/selected_item.png', cropped) | |
return cropped, x, y | |
# No good matches, then return None | |
return None, 0, 0 | |
def capture_region_pct(self, region): | |
""" Grab the image based on the region name/rect. | |
Returns an unfiltered image, either from screenshot or provided image. | |
@param region: The region to check in % (0.0 - 1.0). | |
""" | |
rect = region['rect'] | |
image = self.screen.get_screen_region_pct(rect) | |
return image | |
def is_text_in_selected_item_in_image(self, img, text, min_w, min_h): | |
""" Does the selected item in the region include the text being checked for. | |
Checks if text exists in a region using OCR. | |
Return True if found, False if not and None if no item was selected. | |
@param min_h: Minimum height in pixels. | |
@param min_w: Minimum width in pixels. | |
@param img: The image to check. | |
@param text: The text to find. | |
""" | |
img_selected, x, y = self.get_highlighted_item_in_image(img, min_w, min_h) | |
if img_selected is None: | |
logger.debug(f"Did not find a selected item in the region.") | |
return None | |
ocr_textlist = self.image_simple_ocr(img_selected) | |
# print(str(ocr_textlist)) | |
if text.upper() in str(ocr_textlist): | |
logger.debug(f"Found '{text}' text in item text '{str(ocr_textlist)}'.") | |
return True | |
else: | |
logger.debug(f"Did not find '{text}' text in item text '{str(ocr_textlist)}'.") | |
return False | |
def is_text_in_region(self, text, region): | |
""" Does the region include the text being checked for. The region does not need | |
to include highlighted areas. | |
Checks if text exists in a region using OCR. | |
Return True if found, False if not and None if no item was selected. | |
@param text: The text to check for. | |
@param region: The region to check in % (0.0 - 1.0). | |
""" | |
img = self.capture_region_pct(region) | |
ocr_textlist = self.image_simple_ocr(img) | |
# print(str(ocr_textlist)) | |
if text.upper() in str(ocr_textlist): | |
logger.debug(f"Found '{text}' text in item text '{str(ocr_textlist)}'.") | |
return True | |
else: | |
logger.debug(f"Did not find '{text}' text in item text '{str(ocr_textlist)}'.") | |
return False | |
def select_item_in_list(self, text, region, keys, min_w, min_h) -> bool: | |
""" Attempt to find the item by text in a list defined by the region. | |
If found, leaves it selected for further actions. | |
@param keys: | |
@param text: Text to find. | |
@param region: The region to check in % (0.0 - 1.0). | |
@param min_h: Minimum height in pixels. | |
@param min_w: Minimum width in pixels. | |
""" | |
in_list = False # Have we seen one item yet? Prevents quiting if we have not selected the first item. | |
while 1: | |
img = self.capture_region_pct(region) | |
if img is None: | |
return False | |
found = self.is_text_in_selected_item_in_image(img, text, min_w, min_h) | |
# Check if end of list. | |
if found is None and in_list: | |
logger.debug(f"Did not find '{text}' in {region} list.") | |
return False | |
if found: | |
logger.debug(f"Found '{text}' in {region} list.") | |
return True | |
else: | |
# Next item | |
in_list = True | |
keys.send("UI_Down") | |
def wait_for_text(self, texts: list[str], region, timeout=30) -> bool: | |
""" Wait for a screen to appear by checking for text to appear in the region. | |
@param texts: List of text to check for. Success occurs if any in the list is found. | |
@param region: The region to check in % (0.0 - 1.0). | |
@param timeout: Time to wait for screen in seconds | |
""" | |
start_time = time.time() | |
while True: | |
# Check for timeout. | |
if time.time() > (start_time + timeout): | |
return False | |
# Check if screen has appeared. | |
for text in texts: | |
res = self.is_text_in_region(text, region) | |
if res: | |
return True | |
time.sleep(0.25) | |
--- | |
File: /Overlay.py | |
--- | |
import threading | |
from ctypes.wintypes import PRECT | |
from time import sleep | |
import win32api | |
import win32con | |
import win32gui | |
import win32ui | |
""" | |
File:Overlay.py | |
Description: | |
Class to support drawing lines and text which would overlay the parent screen | |
Author: [email protected] | |
to use: | |
ov = Overlay("") | |
# key = a string to be any identifier you want it to be | |
ov.overlay_rect(key, (pt[0]+x_start, pt[1]+y_start), (pt[0] + compass_width+x_start, pt[1] + compass_height+y_start), (R,G,B), thinkness) | |
ov.overlay_setfont("Times New Roman", 12 ) | |
ov.overlay_set_pos(2000, 50) | |
# row,col, color based on fontSize | |
ov.overlay_text('1', "Hello World", 1, 1,(0,0,255) ) | |
ov.overlay_paint() | |
#@end | |
ov.overlay_quit() | |
""" | |
lines = {} | |
text = {} | |
floating_text = {} | |
fnt = ["Times New Roman", 12, 12] | |
pos = [0,0] | |
elite_dangerous_window = "Elite - Dangerous (CLIENT)" | |
class Vector: | |
def __init__(self, x, y, w, h): | |
self.x = x | |
self.y = y | |
self.w = w | |
self.h = h | |
def __ne__(self, other): | |
this = self.x + self.y + self.w + self.h | |
_other = other.x + other.y + other.w + other.h | |
return this != _other | |
class Overlay: | |
def __init__(self, parent_window, elite=0): | |
self.parent = parent_window | |
if elite == 1: | |
self.parent = elite_dangerous_window | |
self.hWindow = None | |
self.overlay_thr = threading.Thread(target=self.overlay_win32_run) | |
self.overlay_thr.setDaemon(False) | |
self.overlay_thr.start() | |
self.targetRect = Vector(0, 0, 1920, 1080) | |
self.tHwnd = None | |
def overlay_win32_run(self): | |
hInstance = win32api.GetModuleHandle() | |
className = 'OverlayClassName' | |
hWndParent = None | |
if self.parent != "": | |
self.tHwnd= win32gui.FindWindow(None, self.parent) | |
if self.tHwnd: | |
rect = win32gui.GetWindowRect(self.tHwnd) | |
self.targetRect = Vector(rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1]) | |
wndClass = win32gui.WNDCLASS() | |
wndClass.style = win32con.CS_HREDRAW | win32con.CS_VREDRAW | |
wndClass.lpfnWndProc = self.wndProc | |
wndClass.hInstance = hInstance | |
wndClass.hCursor = win32gui.LoadCursor(None, win32con.IDC_ARROW) | |
wndClass.hbrBackground = win32gui.GetStockObject(win32con.WHITE_BRUSH) | |
wndClass.lpszClassName = className | |
wndClassAtom = win32gui.RegisterClass(wndClass) | |
exStyle = win32con.WS_EX_COMPOSITED | win32con.WS_EX_LAYERED | win32con.WS_EX_NOACTIVATE | win32con.WS_EX_TOPMOST | win32con.WS_EX_TRANSPARENT | |
style = win32con.WS_DISABLED | win32con.WS_POPUP | win32con.WS_VISIBLE | |
self.hWindow = win32gui.CreateWindowEx( | |
exStyle, | |
wndClassAtom, | |
None, # WindowName | |
style, | |
0, # x | |
0, # y | |
win32api.GetSystemMetrics(win32con.SM_CXSCREEN), # width | |
win32api.GetSystemMetrics(win32con.SM_CYSCREEN), # height | |
hWndParent, # hWndParent | |
None, # hMenu | |
hInstance, | |
None # lpParam | |
) | |
win32gui.SetLayeredWindowAttributes(self.hWindow, 0x00ffffff, 255, win32con.LWA_COLORKEY | win32con.LWA_ALPHA) | |
win32gui.SetWindowPos(self.hWindow, win32con.HWND_TOPMOST, 0, 0, 0, 0, | |
win32con.SWP_NOACTIVATE | win32con.SWP_NOMOVE | win32con.SWP_NOSIZE | win32con.SWP_SHOWWINDOW) | |
# If a parent was specified, and we found it move our window over that window | |
if self.tHwnd != None: | |
win32gui.MoveWindow(self.hWindow, self.targetRect.x, self.targetRect.y, self.targetRect.w, self.targetRect.h, True) | |
# Dispatch messages, this function needs to be its own task to run forever until Quit | |
win32gui.PumpMessages() | |
def _GetTargetWindowRect(self): | |
rect = win32gui.GetWindowRect(self.tHwnd) | |
ret = Vector(rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1]) | |
return ret | |
def overlay_rect(self, key, pt1, pt2, color, thick): | |
global lines | |
lines[key] = [pt1, pt2, color, thick] | |
def overlay_rect1(self, key, rect, color, thick): | |
global lines | |
lines[key] = [(rect[0], rect[1]), (rect[2], rect[3]), color, thick] | |
def overlay_setfont(self, fontname, fsize ): | |
global fnt | |
fnt = [fontname, fsize, fsize] | |
def overlay_set_pos(self, x, y): | |
global pos | |
pos = [x, y] | |
def overlay_text(self, key, txt, row, col, color): | |
global text | |
text[key] = [txt, row, col, color] | |
def overlay_floating_text(self, key, txt, x, y, color): | |
global floating_text | |
floating_text[key] = [txt, x, y, color] | |
def overlay_paint(self): | |
# if a parent was specified check to see if it moved, if so reposition our origin to new window location | |
if self.tHwnd: | |
if self.targetRect != self._GetTargetWindowRect(): | |
rect = win32gui.GetWindowRect(self.tHwnd) | |
self.targetRect = Vector(rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1]) | |
win32gui.MoveWindow(self.hWindow, self.targetRect.x, self.targetRect.y, self.targetRect.w, self.targetRect.h, True) | |
win32gui.RedrawWindow(self.hWindow, None, None, win32con.RDW_INVALIDATE | win32con.RDW_ERASE) | |
def overlay_clear(self): | |
lines.clear() | |
text.clear() | |
floating_text.clear() | |
def overlay_remove_rect(self, key): | |
if key in lines: | |
lines.pop(key) | |
def overlay_remove_text(self, key): | |
if key in text: | |
text.pop(key) | |
def overlay_remove_floating_text(self, key): | |
if key in floating_text: | |
floating_text.pop(key) | |
def overlay_quit(self): | |
win32gui.PostMessage(self.hWindow, win32con.WM_CLOSE, 0, 0) | |
@staticmethod | |
def overlay_draw_rect(hdc, pt1, pt2, line_type, color, thick): | |
wid = pt2[0] - pt1[0] | |
hgt = pt2[1] - pt1[1] | |
pin_thick = win32gui.CreatePen(line_type, thick, win32api.RGB(color[0], color[1], color[2])) | |
pin_thin = win32gui.CreatePen(line_type, 1, win32api.RGB(color[0], color[1], color[2])) | |
if wid < 20: | |
win32gui.SelectObject(hdc, pin_thin) | |
win32gui.Rectangle(hdc, pt1[0], pt1[1], pt2[0], pt2[1]) | |
else: | |
len_wid = wid / 5 | |
len_hgt = hgt / 5 | |
half_wid = wid/ 2 | |
half_hgt = hgt/ 2 | |
tic_len = thick-1 | |
# top | |
win32gui.SelectObject(hdc, pin_thick) | |
win32gui.MoveToEx(hdc, int(pt1[0]), int(pt1[1])) | |
win32gui.LineTo (hdc, int(pt1[0]+len_wid), int(pt1[1])) | |
win32gui.SelectObject(hdc, pin_thin) | |
win32gui.MoveToEx(hdc, int(pt1[0]+(2*len_wid)), int(pt1[1])) | |
win32gui.LineTo (hdc, int(pt1[0]+(3*len_wid)), int(pt1[1])) | |
win32gui.SelectObject(hdc, pin_thick) | |
win32gui.MoveToEx(hdc, int(pt1[0]+(4*len_wid)), int(pt1[1])) | |
win32gui.LineTo (hdc, int(pt2[0]), int(pt1[1])) | |
# top tic | |
win32gui.MoveToEx(hdc, int(pt1[0]+half_wid), int(pt1[1])) | |
win32gui.LineTo (hdc, int(pt1[0]+half_wid), int(pt1[1])-tic_len) | |
# bot | |
win32gui.MoveToEx(hdc, int(pt1[0]), int(pt2[1])) | |
win32gui.LineTo (hdc, int(pt1[0]+len_wid), int(pt2[1])) | |
win32gui.SelectObject(hdc, pin_thin) | |
win32gui.MoveToEx(hdc, int(pt1[0]+(2*len_wid)), int(pt2[1])) | |
win32gui.LineTo (hdc, int(pt1[0]+(3*len_wid)), int(pt2[1])) | |
win32gui.SelectObject(hdc, pin_thick) | |
win32gui.MoveToEx(hdc, int(pt1[0]+(4*len_wid)), int(pt2[1])) | |
win32gui.LineTo (hdc, int(pt2[0]), int(pt2[1])) | |
# bot tic | |
win32gui.MoveToEx(hdc, int(pt1[0]+half_wid), int(pt2[1])) | |
win32gui.LineTo (hdc, int(pt1[0]+half_wid), int(pt2[1])+tic_len) | |
# left | |
win32gui.MoveToEx(hdc, int(pt1[0]), int(pt1[1])) | |
win32gui.LineTo (hdc, int(pt1[0]), int(pt1[1]+len_hgt)) | |
win32gui.SelectObject(hdc, pin_thin) | |
win32gui.MoveToEx(hdc, int(pt1[0]), int(pt1[1]+(2*len_hgt))) | |
win32gui.LineTo (hdc, int(pt1[0]), int(pt1[1]+(3*len_hgt))) | |
win32gui.SelectObject(hdc, pin_thick) | |
win32gui.MoveToEx(hdc, int(pt1[0]), int(pt1[1]+(4*len_hgt))) | |
win32gui.LineTo (hdc, int(pt1[0]), int(pt2[1])) | |
# left tic | |
win32gui.MoveToEx(hdc, int(pt1[0]), int(pt1[1]+half_hgt)) | |
win32gui.LineTo (hdc, int(pt1[0]-tic_len), int(pt1[1]+half_hgt)) | |
# right | |
win32gui.MoveToEx(hdc, int(pt2[0]), int(pt1[1])) | |
win32gui.LineTo (hdc, int(pt2[0]), int(pt1[1]+len_hgt)) | |
win32gui.SelectObject(hdc, pin_thin) | |
win32gui.MoveToEx(hdc, int(pt2[0]), int(pt1[1]+(2*len_hgt))) | |
win32gui.LineTo (hdc, int(pt2[0]), int(pt1[1]+(3*len_hgt))) | |
win32gui.SelectObject(hdc, pin_thick) | |
win32gui.MoveToEx(hdc, int(pt2[0]), int(pt1[1]+(4*len_hgt))) | |
win32gui.LineTo (hdc, int(pt2[0]), int(pt2[1])) | |
# right tic | |
win32gui.MoveToEx(hdc, int(pt2[0]), int(pt1[1]+half_hgt)) | |
win32gui.LineTo (hdc, int(pt2[0]+tic_len), int(pt1[1]+half_hgt)) | |
@staticmethod | |
def overlay_set_font(hdc, fontname, fontSize): | |
global fnt | |
dpiScale = win32ui.GetDeviceCaps(hdc, win32con.LOGPIXELSX) / 60.0 | |
lf = win32gui.LOGFONT() | |
lf.lfFaceName = fontname | |
lf.lfHeight = int(round(dpiScale * fontSize)) | |
#lf.lfWeight = 150 | |
# Use nonantialiased to remove the white edges around the text. | |
lf.lfQuality = win32con.NONANTIALIASED_QUALITY | |
hf = win32gui.CreateFontIndirect(lf) | |
win32gui.SelectObject(hdc, hf) | |
fnt[2] = lf.lfHeight | |
@staticmethod | |
def overlay_draw_text(hWnd, hdc, txt, row, col, color): | |
global pos, fnt | |
x = pos[0]+col*fnt[1] | |
y = pos[1]+row*fnt[2] | |
rect = (x, y, 1, 1) | |
win32gui.SetTextColor(hdc,win32api.RGB(color[0], color[1], color[2])) | |
win32gui.DrawText(hdc, txt, -1, rect, win32con.DT_LEFT | win32con.DT_NOCLIP | win32con.DT_SINGLELINE | win32con.DT_TOP ) | |
@staticmethod | |
def overlay_draw_floating_text(hWnd, hdc, txt, x, y, color): | |
rect = (x, y, 1, 1) | |
win32gui.SetTextColor(hdc,win32api.RGB(color[0], color[1], color[2])) | |
win32gui.DrawText(hdc, txt, -1, rect, win32con.DT_LEFT | win32con.DT_NOCLIP | win32con.DT_SINGLELINE | win32con.DT_TOP ) | |
@staticmethod | |
def wndProc(hWnd, message, wParam, lParam): | |
global lines, text | |
if message == win32con.WM_PAINT: | |
hdc, paintStruct = win32gui.BeginPaint(hWnd) | |
Overlay.overlay_set_font(hdc, fnt[0], fnt[1]) | |
for i, key in enumerate(lines): | |
#print(lines[key]) | |
Overlay.overlay_draw_rect(hdc, lines[key][0], lines[key][1], win32con.PS_SOLID, lines[key][2], lines[key][3]) | |
for i, key in enumerate(text): | |
#print(text[key]) | |
Overlay.overlay_draw_text(hWnd, hdc, text[key][0], text[key][1], text[key][2], text[key][3]) | |
for i, key in enumerate(floating_text): | |
#print(text[key]) | |
Overlay.overlay_draw_floating_text(hWnd, hdc, floating_text[key][0], floating_text[key][1], | |
floating_text[key][2], floating_text[key][3]) | |
win32gui.EndPaint(hWnd, paintStruct) | |
return 0 | |
elif message == win32con.WM_DESTROY: | |
#print 'Closing the window.' | |
win32gui.PostQuitMessage(0) | |
return 0 | |
else: | |
return win32gui.DefWindowProc(hWnd, message, wParam, lParam) | |
def main(): | |
ov = Overlay("",0) | |
# key x,y x,y end color thinkness | |
rect = {'a': [(50,50), (500, 500), (120, 255, 0),2], | |
'b': [(800,800), (1000, 1000), (20, 10, 255),15] , | |
'c': [(220,30), (350, 700), (255, 20, 10),1] | |
} | |
ov.overlay_setfont("Times New Roman", 12 ) | |
ov.overlay_set_pos(2000, 50) | |
# row,col, color based on fontSize | |
ov.overlay_text('1', "Hello World", 1, 1,(0,0,255) ) | |
ov.overlay_text('2', "next test in line", 2, 1,(255,0,255) ) | |
for i, key in enumerate(rect): | |
ov.overlay_rect(key, rect[key][0], rect[key][1], rect[key][2], rect[key][3]) | |
print("Adding") | |
print(rect[key]) | |
ov.overlay_paint() | |
sleep(5) | |
rect['d'] = [(400,150), (900, 550), (255, 10, 255),25] | |
ov.overlay_rect('d', rect['d'][0], rect['d'][1], rect['d'][2], rect['d'][3]) | |
ov.overlay_text('3', "Changed", 3, 3,(255,0,0) ) | |
ov.overlay_setfont("Times New Roman", 16 ) | |
ov.overlay_set_pos(1800, 50) | |
ov.overlay_paint() | |
sleep(5) | |
rect['b'] = [(40,150), (90, 550), (155, 10, 255),10] | |
ov.overlay_rect('b', rect['b'][0], rect['b'][1], rect['b'][2], rect['b'][3]) | |
ov.overlay_text('3', "", 3, 3,(255,0,0) ) | |
ov.overlay_paint() | |
sleep(5) | |
ov.overlay_quit() | |
sleep(2) | |
if __name__ == "__main__": | |
main() | |
--- | |
File: /Robigo.py | |
--- | |
from time import sleep | |
import time | |
''' | |
import keyboard | |
import win32gui | |
import Screen_Regions | |
from ED_AP import * | |
from EDJournal import * | |
from EDKeys import * | |
from EDlogger import logger | |
from Image_Templates import * | |
from Overlay import * | |
from Screen import * | |
from Voice import * | |
''' | |
""" | |
File:Robigo.py | |
Description: | |
This class contains the script required to execute the Robigo passenger mission loop | |
Constraints: | |
- Odyssey only (due to unique Mission Selection and Mission Completion menus) | |
- In first run most verify that Sirius Atmospherics is in the Nav Panel after entering the Sothis System | |
- if not 'end' the Robigo Assist, Target 'Don's Inheritence' Supercuise there, when < 1000 ls | |
Sirius Athmospherics will be in the Nav Panel, select it, and restart Robigo Assist | |
- Set Nav Menu Filter to: Stations and POI only | |
- Removes the clutter and allows faster selection of Robigo Mines and Sirius Athmos | |
- Set refuelthreshold low.. like 35% so not attempt refuel with a ship that doesn't have fuel scoop | |
Author: [email protected] | |
""" | |
# TODO: | |
# - Robigo Mines SC: Sometimes Robigo mines are other side of the ring when approaching | |
# - to fix this for Horizon, would have to write new routines for get_missions() and complete_missions | |
# then similar to Waypoints use the if is_odyssey != True: to call the right routine | |
# | |
# define the states of the Robigo loop, allow to reenter same state left off if | |
# having to cancel the AP but want to restart where you left off | |
STATE_MISSIONS = 1 | |
STATE_ROUTE_TO_SOTHIS = 2 | |
STATE_UNDOCK = 3 | |
STATE_FSD_TO_SOTHIS = 4 | |
STATE_TARGET_SIRIUS = 5 | |
STATE_SC_TO_SIRIUS = 6 | |
STATE_ROUTE_TO_ROBIGO = 7 | |
STATE_FSD_TO_ROBIGO = 8 | |
STATE_TARGET_ROBIGO_MINES = 9 | |
STATE_SC_TO_ROBIGO_MINES = 10 | |
class Robigo: | |
def __init__(self, ed_ap): | |
self.ap = ed_ap | |
self.mission_redirect = 0 | |
self.mission_complete = 0 | |
self.state = STATE_MISSIONS # default to turn in missions and get more | |
self.do_single_loop = False # default is to continue to loop | |
def set_single_loop(self, single_loop): | |
self.do_single_loop = single_loop | |
# | |
# This function will look to see if the passed in template is in the region of the screen specified | |
def is_found(self, ap, region, templ) -> bool: | |
(img, | |
(minVal, maxVal, minLoc, maxLoc), | |
match, | |
) = ap.scrReg.match_template_in_region(region, templ) | |
#if maxVal > 75: | |
# print("Image Match: "+templ+" " + str(maxVal)) | |
# use high percent | |
if maxVal > 0.75: | |
return True | |
else: | |
return False | |
# Turn in Missions, will do Credit only | |
def complete_missions(self, ap): | |
# get how many missions were completed, @sirius atmospherics, this mission_redirected will | |
# update for each mission you selected | |
loop_missions = ap.jn.ship_state()['mission_redirected'] | |
if loop_missions == 0: | |
loop_missions = 8 | |
ap.jn.ship_state()['mission_completed'] = 0 | |
# Asssume in passenger Lounge | |
ap.keys.send("UI_Right", repeat=2) # go to Complete Mission | |
ap.keys.send("UI_Select") | |
sleep(2) # give time for mission page to come up | |
found = self.is_found(ap, "missions", "missions") | |
# we don't have any missions to complete as the screen did not change | |
if found: | |
print("no missions to complete") | |
ap.keys.send("UI_Left") | |
return | |
ap.keys.send("UI_Up", repeat=2) # goto the top | |
ap.keys.send("UI_Down") # down one to select first mission | |
sleep(0.5) | |
for i in range(loop_missions): | |
ap.keys.send("UI_Select") # select mission | |
sleep(0.1) | |
ap.keys.send("UI_Up") # Up to Credit | |
sleep(0.1) | |
ap.keys.send("UI_Select") # Select it | |
sleep(10) # wait until the "skip" button changes to "back" button | |
ap.keys.send("UI_Select") # Select the Back key which will be highlighted | |
sleep(1.5) | |
ap.keys.send("UI_Back") # seem to be going back to Mission menu | |
# Goto Nav Panel and do image matching on the passed in image template | |
def lock_target(self, ap, templ) -> bool: | |
found = False | |
tries = 0 | |
# get to the Left Panel menu: Navigation | |
ap.keys.send("UI_Back", repeat=10) | |
ap.keys.send("HeadLookReset") | |
ap.keys.send("UIFocus", state=1) | |
ap.keys.send("UI_Left") | |
ap.keys.send("UIFocus", state=0) # this gets us over to the Nav panel | |
sleep(0.5) | |
# | |
ap.keys.send("UI_Down", hold=2) # got to bottom row | |
# tries is the number of rows to go through to find the item looking for | |
# the Nav Panel should be filtered to reduce the number of rows in the list | |
while not found and tries < 50: | |
found = self.is_found(ap, "nav_panel", templ) | |
if found: | |
ap.keys.send("UI_Select", repeat=2) # Select it and lock target | |
else: | |
tries += 1 | |
ap.keys.send("UI_Up") # up to next item | |
sleep(0.2) | |
ap.keys.send("UI_Back", repeat=10) # go back and drop Nav Panel | |
ap.keys.send("HeadLookReset") | |
return found | |
# Finish the selection of the mission by assigning to a Cabin | |
# Will use the Auto fill which doesn't do a very good job optimizing the cabin fill | |
def select_mission(self, ap): | |
ap.keys.send("UI_Select", repeat=2) # select mission and Pick Cabin | |
ap.keys.send("UI_Down") # move down to Auto Fill line | |
sleep(0.1) | |
ap.keys.send("UI_Right", repeat=2) # go over to "Auto Fill" | |
ap.keys.send("UI_Select") # Select auto fill | |
sleep(0.1) | |
ap.keys.send("UI_Select") # Select Accept Mission, which was auto highlighted | |
# Goes through the missions selecting any that are Sirius Atmos | |
def get_missions(self, ap): | |
cnt = 0 | |
mission_cnt = 0 | |
had_selected = False | |
# Asssume in passenger Lounge. goto Missions Menu | |
ap.keys.send("UI_Up", repeat=3) | |
sleep(0.2) | |
ap.keys.send("UI_Down", repeat=2) | |
sleep(0.2) | |
ap.keys.send("UI_Select") # select personal transport | |
sleep(15) # wait 15 second for missions menu to show up | |
# Loop selecting missions, go up to 20 times, have seen at time up to 17 missions | |
# before getting to Sirius Atmos missions | |
while cnt < 20: | |
found = self.is_found(ap, "mission_dest", "dest_sirius") | |
# not a sirius mission | |
if not found: | |
ap.keys.send("UI_Down") # not sirius, go down to next | |
sleep(0.1) | |
# if we had selected missions and suddenly we are not | |
# then go back to top and scroll down again. This is due to scrolling | |
# missions | |
if had_selected: | |
ap.keys.send("UI_Up", hold=2) # go back to very top | |
sleep(0.5) | |
cnt = 1 # reset counter | |
had_selected = False | |
cnt = cnt + 1 | |
else: | |
mission_cnt += 1 # found a mission, select it | |
had_selected = True | |
self.select_mission(ap) | |
sleep(1.5) | |
ap.keys.send("UI_Back", repeat=4) # go back to main menu | |
# Go to the passenger lounge menu | |
def goto_passenger_lounge(self, ap): | |
# Go down to station services and select | |
ap.keys.send("UI_Back", repeat=5) # make sure at station menu | |
ap.keys.send("UI_Up", hold=2) # go to very top | |
ap.keys.send("UI_Down") | |
ap.keys.send("UI_Select") | |
sleep(6) # give time for Mission Board to come up | |
ap.keys.send("UI_Up") # ensure at Missio Board | |
ap.keys.send("UI_Left") # | |
ap.keys.send("UI_Select") # select Mission Board | |
sleep(1) | |
ap.keys.send("UI_Right") # Passenger lounge | |
sleep(0.1) | |
ap.keys.send("UI_Select") | |
sleep(2) # give time to bring up menu | |
# SC to the Marker and retrieve num of missions redirected (i.e. completed) | |
def travel_to_sirius_atmos(self, ap): | |
ap.jn.ship_state()['mission_redirected'] = 0 | |
self.mission_redirected = 0 | |
# at times when coming out of SC, we are still 700-800km away from Sirius Atmos | |
# if we do not get mission_redirected journal notices, then SC again | |
while self.mission_redirected == 0: | |
# SC to Marker, tell assist not to attempt docking since we are going to a marker | |
ap.sc_assist(ap.scrReg, do_docking=False) | |
# wait until we are back in_space | |
while ap.jn.ship_state()['status'] != 'in_space': | |
sleep(1) | |
# give a few more seconds | |
sleep(2) | |
ap.keys.send("SetSpeedZero") | |
ap.keys.send("SelectTarget") # target the marker so missions will complete | |
# takes about 10-22 sec to acknowledge missions | |
sleep(15) | |
if ap.jn.ship_state()['mission_redirected'] == 0: | |
print("Didnt make it to sirius atmos, should SC again") | |
self.mission_redirected = ap.jn.ship_state()['mission_redirected'] | |
# Determine where we are at in the Robigo Loop, return the corresponding state | |
def determine_state(self, ap) -> int: | |
state = STATE_MISSIONS | |
status = ap.jn.ship_state()['status'] | |
target = ap.jn.ship_state()['target'] | |
location = ap.jn.ship_state()['location'] | |
body = ap.jn.ship_state()['body'] | |
if status == 'in_station': | |
state = STATE_MISSIONS | |
if target != None: # seems we already have a target | |
state = STATE_UNDOCK | |
elif status == 'in_space' and location == 'Robigo' and target != None: | |
state = STATE_FSD_TO_SOTHIS | |
elif status == 'in_space' and location == 'Sothis' and body == 'Sothis A 5': | |
if target == None: | |
state = STATE_ROUTE_TO_ROBIGO | |
else: | |
state = STATE_FSD_TO_ROBIGO | |
elif location == 'Sothis' and status == 'in_supercruise': | |
state = STATE_SC_TO_SIRIUS | |
elif location == 'Robigo' and status == 'in_supercruise': | |
state = STATE_SC_TO_ROBIGO_MINES | |
elif location != 'Robigo' and location != 'Sothis': | |
# we are in neither system, and using default state, so lets target | |
# Robigo and FSD there | |
if self.state == STATE_MISSIONS: # default state, then go back to Robigo | |
state = STATE_ROUTE_TO_ROBIGO | |
else: | |
# else we are between the systems, so lets just use the last state we were in | |
# either FSD_TO_ROBIGO or FSD_TO_SOTHIS | |
state = self.state # other wise go back to last state | |
return state | |
# The Robigo Loop | |
def loop(self, ap): | |
loop_cnt = 0 | |
starttime = time.time() | |
self.state = self.determine_state(ap) | |
while True: | |
if self.state == STATE_MISSIONS: | |
if self.do_single_loop == False: # if looping, then do mission processing | |
ap.update_ap_status("Completing missions") | |
# Complete Missions, if we have any | |
self.goto_passenger_lounge(ap) | |
sleep(2.5) # wait for new menu comes up | |
self.complete_missions(ap) | |
ap.update_ap_status("Get missions") | |
# Select and fill up on Sirius missions | |
self.goto_passenger_lounge(ap) | |
sleep(1) | |
self.get_missions(ap) | |
self.state = STATE_ROUTE_TO_SOTHIS | |
elif self.state == STATE_ROUTE_TO_SOTHIS: | |
ap.update_ap_status("Route to SOTHIS") | |
# Target SOTHIS and plot route | |
ap.jn.ship_state()["target"] = None # must clear out previous target from Journal | |
dest = ap.galaxy_map.set_gal_map_destination_text(ap, "SOTHIS ", target_select_cb=ap.jn.ship_state) | |
if dest == False: | |
ap.update_ap_status("SOTHIS not set: " + str(dest)) | |
break | |
sleep(1) # give time to popdown GalaxyMap | |
self.state = STATE_UNDOCK | |
elif self.state == STATE_UNDOCK: | |
# if we got the destination and perform undocking | |
ap.keys.send("SetSpeedZero") # ensure 0 so auto undock will work | |
ap.waypoint_undock_seq() | |
self.state = STATE_FSD_TO_SOTHIS | |
elif self.state == STATE_FSD_TO_SOTHIS: | |
ap.update_ap_status("FSD to SOTHIS") | |
# away from station, time for Route Assist to get us to SOTHIS | |
ap.fsd_assist(ap.scrReg) | |
ap.keys.send("SetSpeed50") # reset speed | |
self.state = STATE_TARGET_SIRIUS | |
elif self.state == STATE_TARGET_SIRIUS: | |
ap.update_ap_status("Target Sirius") | |
# [In Sothis] | |
# select Siruis Atmos | |
found = self.lock_target(ap, 'sirius_atmos') | |
if found == False: | |
ap.update_ap_status("No Sirius Atmos in Nav Panel") | |
return | |
self.state = STATE_SC_TO_SIRIUS | |
elif self.state == STATE_SC_TO_SIRIUS: | |
ap.update_ap_status("SC to Marker") | |
self.travel_to_sirius_atmos(ap) | |
ap.update_ap_status("Missions: "+str(self.mission_redirected)) | |
self.state = STATE_ROUTE_TO_ROBIGO | |
elif self.state == STATE_ROUTE_TO_ROBIGO: | |
ap.update_ap_status("Route to Robigo") | |
# Set Route back to Robigo | |
dest = ap.galaxy_map.set_gal_map_destination_text(ap, "ROBIGO", target_select_cb=ap.jn.ship_state) | |
sleep(2) | |
if dest == False: | |
ap.update_ap_status("Robigo not set: " + str(dest)) | |
break | |
self.state = STATE_FSD_TO_ROBIGO | |
elif self.state == STATE_FSD_TO_ROBIGO: | |
ap.update_ap_status("FSD to Robigo") | |
# have Route Assist bring us back to Robigo system | |
ap.fsd_assist(ap.scrReg) | |
ap.keys.send("SetSpeed50") | |
sleep(2) | |
self.state = STATE_TARGET_ROBIGO_MINES | |
elif self.state == STATE_TARGET_ROBIGO_MINES: | |
ap.update_ap_status("Target Robigo Mines") | |
# In Robigo System, select Robigo Mines in the Nav Panel, which should 1 down (we select in the blind) | |
# The Robigo Mines position in the list is dependent on distance from current location | |
# The Speed50 above and waiting 2 seconds ensures Robigo Mines is 2 down from top | |
found = self.lock_target(ap, 'robigo_mines') | |
if found == False: | |
ap.update_ap_status("No lock on Robigo Mines in Nav Panel") | |
return | |
self.state = STATE_SC_TO_ROBIGO_MINES | |
elif self.state == STATE_SC_TO_ROBIGO_MINES: | |
ap.update_ap_status("SC to Robigo Mines") | |
# SC Assist to Robigo Mines and dock | |
ap.sc_assist(ap.scrReg) | |
# Calc elapsed time for a loop and display it | |
elapsed_time = time.time() - starttime | |
starttime = time.time() | |
loop_cnt += 1 | |
if loop_cnt != 0: | |
ap.ap_ckb('log',"Loop: "+str(loop_cnt)+" Time: "+ time.strftime("%H:%M:%S", time.gmtime(elapsed_time))) | |
self.state = STATE_MISSIONS | |
if self.do_single_loop == True: # we did one loop, return | |
ap.update_ap_status("Single Loop Complete") | |
return | |
# Useful Journal data that might be able to leverage | |
# if didn't make it to Robigo Mines (at the ring), need to retry | |
#"timestamp":"2022-05-08T23:49:43Z", "event":"SupercruiseExit", "Taxi":false, "Multicrew":false, "StarSystem":"Robigo", "SystemAddress":9463020987689, "Body":"Robigo 1 A Ring", "BodyID":12, "BodyType":"PlanetaryRing" } | |
#This entry shows we made it to the Robigo Mines | |
#{ "timestamp":"2022-05-08T23:51:43Z", "event":"SupercruiseExit", "Taxi":false, "Multicrew":false, "StarSystem":"Robigo", "SystemAddress":9463020987689, "Body":"Robigo Mines", "BodyID":64, "BodyType":"Station" } | |
# if we get these then we got mission complete and made it to Sirius Atmos, if not we are short | |
# "event":"MissionRedirected", "NewDestinationStation":"Robigo Mines", "NewDestinationSystem":"Robigo", "OldDestinationStation":"", "OldDestinationSystem":"Sothis" } | |
# if didn't make it to Sirius Atmos, go back into SC | |
#{ "timestamp":"2022-05-08T22:01:27Z", "event":"SupercruiseExit", "Taxi":false, "Multicrew":false, "StarSystem":"Sothis", "SystemAddress":3137146456387, "Body":"Sothis A 5", "BodyID":14, "BodyType":"Planet" } | |
# -------------------------------------------------------- | |
''' | |
Test Code pre-integration with the GUI, run standalone | |
global main_thrd | |
global ap1 | |
class EDAP_Interrupt(Exception): | |
pass | |
def key_callback(name): | |
global ap1, main_thrd | |
print("exiting") | |
# if name == "end": | |
ap1.ctype_async_raise(main_thrd, EDAP_Interrupt) | |
def callback(self, key, body=None): | |
pass | |
# print("cb:"+str(body)) | |
def main(): | |
global main_thrd , ap1 | |
main_thrd = threading.current_thread() | |
ap = EDAutopilot(callback, False) | |
ap1 = ap | |
# assume python | |
ap.rollrate = 104.0 | |
ap.pitchrate = 33.0 | |
ap.yawrate = 11.0 | |
ap.sunpitchuptime = 1.0 | |
robigo = Robigo(ap) | |
ap.terminate = True # no need for the AP's engine loop to run | |
# trap specific hotkeys | |
keyboard.add_hotkey("home", key_callback, args=("q")) | |
keyboard.add_hotkey("end", key_callback, args=("s")) | |
# print("Going to sleep") | |
try: | |
robigo.loop(ap) | |
except EDAP_Interrupt: | |
logger.debug("Caught stop exception") | |
except Exception as e: | |
print("Trapped generic:" + str(e)) | |
traceback.print_exc() | |
print("Terminating") | |
ap.overlay.overlay_quit() | |
ap.vce.quit() | |
if __name__ == "__main__": | |
main() | |
''' | |
--- | |
File: /Screen_Regions.py | |
--- | |
from numpy import array, sum | |
import cv2 | |
""" | |
File:Screen_Regions.py | |
Description: | |
Class to rectangle areas of the screen to capture along with filters to apply. Includes functions to | |
match a image template to the region using opencv | |
Author: [email protected] | |
""" | |
def size_scale_for_station(width: int, height: int, w: int, h: int) -> (int, int): | |
""" Scale an item in the station services region based on the target resolution. | |
This is performed because the tables on the station services screen do | |
not increase proportionally with the screen size. The width changes with | |
the screen size, the height does not change based on the screen size | |
height, but on the screen width and the position stays consistent to the | |
center of the screen. | |
To calculate the new region height, we take the initial region defined at | |
1920x1080 and scale up the height based on the target width and apply the | |
new proportion against the center line. | |
@param width: The width of the item in pixels | |
@param height: The height of the item in pixels | |
@param h: The screen height in pixels | |
@param w: The screen width in pixels | |
""" | |
ref_w = 1920 | |
ref_h = 1080 | |
# Calc the x and y scaling. | |
x_scale = w / ref_w | |
y_scale = h / ref_h | |
# Increase the height by the ratio of the width | |
new_width = width * x_scale | |
new_height = height * x_scale | |
# Return the new height in pixels. | |
return new_width, new_height | |
class Screen_Regions: | |
def __init__(self, screen, templ): | |
self.screen = screen | |
self.templates = templ | |
# Define the thresholds for template matching to be consistent throughout the program | |
self.compass_match_thresh = 0.5 | |
self.navpoint_match_thresh = 0.8 | |
self.target_thresh = 0.54 | |
self.target_occluded_thresh = 0.55 | |
self.sun_threshold = 125 | |
self.disengage_thresh = 0.25 | |
# array is in HSV order which represents color ranges for filtering | |
self.orange_color_range = [array([0, 130, 123]), array([25, 235, 220])] | |
self.orange_2_color_range = [array([16, 165, 220]), array([98, 255, 255])] | |
self.target_occluded_range= [array([16, 31, 85]), array([26, 160, 212])] | |
self.blue_color_range = [array([0, 28, 170]), array([180, 100, 255])] | |
self.blue_sco_color_range = [array([10, 0, 0]), array([100, 150, 255])] | |
self.fss_color_range = [array([95, 210, 70]), array([105, 255, 120])] | |
self.reg = {} | |
# regions with associated filter and color ranges | |
# The rect is top left x, y, and bottom right x, y in fraction of screen resolution | |
self.reg['compass'] = {'rect': [0.33, 0.65, 0.46, 1.0], 'width': 1, 'height': 1, 'filterCB': self.equalize, 'filter': None} | |
self.reg['target'] = {'rect': [0.33, 0.27, 0.66, 0.70], 'width': 1, 'height': 1, 'filterCB': self.filter_by_color, 'filter': self.orange_2_color_range} # also called destination | |
self.reg['target_occluded'] = {'rect': [0.33, 0.27, 0.66, 0.70], 'width': 1, 'height': 1, 'filterCB': self.filter_by_color, 'filter': self.target_occluded_range} | |
self.reg['sun'] = {'rect': [0.30, 0.30, 0.70, 0.68], 'width': 1, 'height': 1, 'filterCB': self.filter_sun, 'filter': None} | |
self.reg['disengage'] = {'rect': [0.42, 0.65, 0.60, 0.80], 'width': 1, 'height': 1, 'filterCB': self.filter_by_color, 'filter': self.blue_sco_color_range} | |
self.reg['sco'] = {'rect': [0.42, 0.65, 0.60, 0.80], 'width': 1, 'height': 1, 'filterCB': self.filter_by_color, 'filter': self.blue_sco_color_range} | |
self.reg['fss'] = {'rect': [0.5045, 0.7545, 0.532, 0.7955], 'width': 1, 'height': 1, 'filterCB': self.equalize, 'filter': None} | |
self.reg['mission_dest'] = {'rect': [0.46, 0.38, 0.65, 0.86], 'width': 1, 'height': 1, 'filterCB': self.equalize, 'filter': None} | |
self.reg['missions'] = {'rect': [0.50, 0.78, 0.65, 0.85], 'width': 1, 'height': 1, 'filterCB': self.equalize, 'filter': None} | |
self.reg['nav_panel'] = {'rect': [0.25, 0.36, 0.60, 0.85], 'width': 1, 'height': 1, 'filterCB': self.equalize, 'filter': None} | |
# convert rect from percent of screen into pixel location, calc the width/height of the area | |
for i, key in enumerate(self.reg): | |
xx = self.reg[key]['rect'] | |
self.reg[key]['rect'] = [int(xx[0]*screen.screen_width), int(xx[1]*screen.screen_height), | |
int(xx[2]*screen.screen_width), int(xx[3]*screen.screen_height)] | |
self.reg[key]['width'] = self.reg[key]['rect'][2] - self.reg[key]['rect'][0] | |
self.reg[key]['height'] = self.reg[key]['rect'][3] - self.reg[key]['rect'][1] | |
def capture_region(self, screen, region_name): | |
""" Just grab the screen based on the region name/rect. | |
Returns an unfiltered image. """ | |
return screen.get_screen_region(self.reg[region_name]['rect']) | |
def capture_region_filtered(self, screen, region_name, inv_col=True): | |
""" Grab screen region and call its filter routine. | |
Returns the filtered image. """ | |
scr = screen.get_screen_region(self.reg[region_name]['rect'], inv_col) | |
if self.reg[region_name]['filterCB'] == None: | |
# return the screen region untouched in BGRA format. | |
return scr | |
else: | |
# return the screen region in the format returned by the filter. | |
return self.reg[region_name]['filterCB'] (scr, self.reg[region_name]['filter']) | |
def match_template_in_region(self, region_name, templ_name, inv_col=True): | |
""" Attempt to match the given template in the given region which is filtered using the region filter. | |
Returns the filtered image, detail of match and the match mask. """ | |
img_region = self.capture_region_filtered(self.screen, region_name, inv_col) # which would call, reg.capture_region('compass') and apply defined filter | |
match = cv2.matchTemplate(img_region, self.templates.template[templ_name]['image'], cv2.TM_CCOEFF_NORMED) | |
(minVal, maxVal, minLoc, maxLoc) = cv2.minMaxLoc(match) | |
return img_region, (minVal, maxVal, minLoc, maxLoc), match | |
def match_template_in_image(self, image, template): | |
""" Attempt to match the given template in the (unfiltered) image. | |
Returns the original image, detail of match and the match mask. """ | |
match = cv2.matchTemplate(image, self.templates.template[template]['image'], cv2.TM_CCOEFF_NORMED) | |
(minVal, maxVal, minLoc, maxLoc) = cv2.minMaxLoc(match) | |
return image, (minVal, maxVal, minLoc, maxLoc), match | |
def equalize(self, image=None, noOp=None): | |
# Load the image in greyscale | |
img_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
# create a CLAHE object (Arguments are optional). Histogram equalization, improves constrast | |
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) | |
img_out = clahe.apply(img_gray) | |
return img_out | |
def filter_by_color(self, image, color_range): | |
"""Filters an image based on a given color range. | |
Returns the filtered image. Pixels within the color range are returned | |
their original color, otherwise black.""" | |
# converting from BGR to HSV color space | |
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) | |
# filter passed in color low, high | |
filtered = cv2.inRange(hsv, color_range[0], color_range[1]) | |
return filtered | |
# not used | |
def filter_bright(self, image=None, noOp=None): | |
equalized = self.equalize(image) | |
equalized = cv2.cvtColor(equalized, cv2.COLOR_GRAY2BGR) #hhhmm, equalize() already converts to gray | |
equalized = cv2.cvtColor(equalized, cv2.COLOR_BGR2HSV) | |
filtered = cv2.inRange(equalized, array([0, 0, 215]), array([0, 0, 255])) #only high value | |
return filtered | |
def set_sun_threshold(self, thresh): | |
self.sun_threshold = thresh | |
# need to compare filter_sun with filter_bright | |
def filter_sun(self, image=None, noOp=None): | |
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
# set low end of filter to 25 to pick up the dull red Class L stars | |
(thresh, blackAndWhiteImage) = cv2.threshold(hsv, self.sun_threshold, 255, cv2.THRESH_BINARY) | |
return blackAndWhiteImage | |
# percent the image is white | |
def sun_percent(self, screen): | |
blackAndWhiteImage = self.capture_region_filtered(screen, 'sun') | |
wht = sum(blackAndWhiteImage == 255) | |
blk = sum(blackAndWhiteImage != 255) | |
result = int((wht / (wht+blk))*100) | |
return result | |
--- | |
File: /Screen.py | |
--- | |
from __future__ import annotations | |
import typing | |
import cv2 | |
import win32gui | |
from numpy import array | |
import mss | |
import json | |
from EDlogger import logger | |
""" | |
File:Screen.py | |
Description: | |
Class to handle screen grabs | |
Author: [email protected] | |
""" | |
# size() return (ctypes.windll.user32.GetSystemMetrics(0), ctypes.windll.user32.GetSystemMetrics(1)) | |
# TODO: consider update to handle ED running in a window | |
# find the ED Window | |
# win32gui.SetForegroundWindow(hwnd) | |
# bbox = win32gui.GetWindowRect(hwnd) will also then give me the resolution of the image | |
# img = ImageGrab.grab(bbox) | |
elite_dangerous_window = "Elite - Dangerous (CLIENT)" | |
class Screen: | |
def __init__(self, cb): | |
self.ap_ckb = cb | |
self.mss = mss.mss() | |
self.using_screen = True # True to use screen, false to use an image. Set screen_image to the image | |
self._screen_image = None # Screen image captured from screen, or loaded by user for testing. | |
# Find ED window position to determine which monitor it is on | |
ed_rect = self.get_elite_window_rect() | |
if ed_rect is None: | |
self.ap_ckb('log', f"ERROR: Could not find window {elite_dangerous_window}.") | |
logger.error(f'Could not find window {elite_dangerous_window}.') | |
else: | |
logger.debug(f'Found Elite Dangerous window position: {ed_rect}') | |
# Examine all monitors to determine match with ED | |
self.mons = self.mss.monitors | |
mon_num = 0 | |
for item in self.mons: | |
if mon_num > 0: # ignore monitor 0 as it is the complete desktop (dims of all monitors) | |
logger.debug(f'Found monitor {mon_num} with details: {item}') | |
if ed_rect is None: | |
self.monitor_number = mon_num | |
self.mon = self.mss.monitors[self.monitor_number] | |
logger.debug(f'Defaulting to monitor {mon_num}.') | |
self.screen_width = item['width'] | |
self.screen_height = item['height'] | |
break | |
else: | |
if item['left'] == ed_rect[0] and item['top'] == ed_rect[1]: | |
# Get information of monitor 2 | |
self.monitor_number = mon_num | |
self.mon = self.mss.monitors[self.monitor_number] | |
logger.debug(f'Elite Dangerous is on monitor {mon_num}.') | |
self.screen_width = item['width'] | |
self.screen_height = item['height'] | |
# Next monitor | |
mon_num = mon_num + 1 | |
# Add new screen resolutions here with tested scale factors | |
# this table will be default, overwritten when loading resolution.json file | |
self.scales = { # scaleX, scaleY | |
'1024x768': [0.39, 0.39], # tested, but not has high match % | |
'1080x1080': [0.5, 0.5], # fix, not tested | |
'1280x800': [0.48, 0.48], # tested | |
'1280x1024': [0.5, 0.5], # tested | |
'1600x900': [0.6, 0.6], # tested | |
'1920x1080': [0.75, 0.75], # tested | |
'1920x1200': [0.73, 0.73], # tested | |
'1920x1440': [0.8, 0.8], # tested | |
'2560x1080': [0.75, 0.75], # tested | |
'2560x1440': [1.0, 1.0], # tested | |
'3440x1440': [1.0, 1.0], # tested | |
# 'Calibrated': [-1.0, -1.0] | |
} | |
# used this to write the self.scales table to the json file | |
# self.write_config(self.scales) | |
ss = self.read_config() | |
# if we read it then point to it, otherwise use the default table above | |
if ss is not None: | |
self.scales = ss | |
logger.debug("read json:"+str(ss)) | |
# try to find the resolution/scale values in table | |
# if not, then take current screen size and divide it out by 3440 x1440 | |
try: | |
scale_key = str(self.screen_width)+"x"+str(self.screen_height) | |
self.scaleX = self.scales[scale_key][0] | |
self.scaleY = self.scales[scale_key][1] | |
except: | |
# if we don't have a definition for the resolution then use calculation | |
self.scaleX = self.screen_width / 3440.0 | |
self.scaleY = self.screen_height / 1440.0 | |
# if the calibration scale values are not -1, then use those regardless of above | |
# if self.scales['Calibrated'][0] != -1.0: | |
# self.scaleX = self.scales['Calibrated'][0] | |
# if self.scales['Calibrated'][1] != -1.0: | |
# self.scaleY = self.scales['Calibrated'][1] | |
logger.debug('screen size: '+str(self.screen_width)+" "+str(self.screen_height)) | |
logger.debug('Default scale X, Y: '+str(self.scaleX)+", "+str(self.scaleY)) | |
@staticmethod | |
def get_elite_window_rect() -> typing.Tuple[int, int, int, int] | None: | |
""" Gets the ED window rectangle. | |
Returns (left, top, right, bottom) or None. | |
""" | |
hwnd = win32gui.FindWindow(None, elite_dangerous_window) | |
if hwnd: | |
rect = win32gui.GetWindowRect(hwnd) | |
return rect | |
else: | |
return None | |
@staticmethod | |
def elite_window_exists() -> bool: | |
""" Does the ED Client Window exist (i.e. is ED running) | |
""" | |
hwnd = win32gui.FindWindow(None, elite_dangerous_window) | |
if hwnd: | |
return True | |
else: | |
return False | |
def write_config(self, data, fileName='./configs/resolution.json'): | |
if data is None: | |
data = self.scales | |
try: | |
with open(fileName,"w") as fp: | |
json.dump(data,fp, indent=4) | |
except Exception as e: | |
logger.warning("Screen.py write_config error:"+str(e)) | |
def read_config(self, fileName='./configs/resolution.json'): | |
s = None | |
try: | |
with open(fileName,"r") as fp: | |
s = json.load(fp) | |
except Exception as e: | |
logger.warning("Screen.py read_config error :"+str(e)) | |
return s | |
# reg defines a box as a percentage of screen width and height | |
def get_screen_region(self, reg, inv_col=True): | |
image = self.get_screen(int(reg[0]), int(reg[1]), int(reg[2]), int(reg[3]), inv_col) | |
return image | |
def get_screen(self, x_left, y_top, x_right, y_bot, inv_col=True): # if absolute need to scale?? | |
monitor = { | |
"top": self.mon["top"] + y_top, | |
"left": self.mon["left"] + x_left, | |
"width": x_right - x_left, | |
"height": y_bot - y_top, | |
"mon": self.monitor_number, | |
} | |
image = array(self.mss.grab(monitor)) | |
# TODO - mss.grab returns the image in BGR format, so no need to convert to RGB2BGR | |
if inv_col: | |
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) | |
return image | |
def get_screen_region_pct(self, region): | |
""" Grabs a screenshot and returns the selected region as an image. | |
@param region: The region to check in % (0.0 - 1.0). | |
""" | |
if self.using_screen: | |
abs_rect = self.screen_pct_to_abs(region) | |
image = self.get_screen(abs_rect[0], abs_rect[1], abs_rect[2], abs_rect[3]) | |
# TODO delete this line when COLOR_RGB2BGR is removed from get_screen() | |
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) | |
return image | |
else: | |
if self._screen_image is None: | |
return None | |
image = self.crop_image_by_pct(self._screen_image, region) | |
return image | |
def screen_pct_to_abs(self, reg): | |
""" Converts and array of real percentage screen values to int absolutes. """ | |
abs_rect = [int(reg[0] * self.screen_width), int(reg[1] * self.screen_height), | |
int(reg[2] * self.screen_width), int(reg[3] * self.screen_height)] | |
return abs_rect | |
def get_screen_full(self): | |
""" Grabs a full screenshot and returns the image. | |
""" | |
if self.using_screen: | |
image = self.get_screen(0, 0, self.screen_width, self.screen_height) | |
# TODO delete this line when COLOR_RGB2BGR is removed from get_screen() | |
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) | |
return image | |
else: | |
if self._screen_image is None: | |
return None | |
return self._screen_image | |
def crop_image_by_pct(self, image, rect): | |
""" Crop an image using a percentage values (0.0 - 1.0). | |
Rect is an array of crop % [0.10, 0.20, 0.90, 0.95] = [Left, Top, Right, Bottom] | |
Returns the cropped image. """ | |
# Existing size | |
h, w, ch = image.shape | |
# Crop to leave only the selected rectangle | |
x0 = int(w * rect[0]) | |
y0 = int(h * rect[1]) | |
x1 = int(w * rect[2]) | |
y1 = int(h * rect[3]) | |
# Crop image | |
cropped = image[y0:y1, x0:x1] | |
return cropped | |
def crop_image(self, image, rect): | |
""" Crop an image using a pixel values. | |
Rect is an array of pixel values [100, 200, 1800, 1600] = [X0, Y0, X1, Y1] | |
Returns the cropped image.""" | |
cropped = image[rect[1]:rect[3], rect[0]:rect[2]] # i.e. [y:y+h, x:x+w] | |
return cropped | |
def set_screen_image(self, image): | |
""" Use an image instead of a screen capture. Sets the image and also sets the | |
screen width and height to the image properties. | |
@param image: The image to use. | |
""" | |
self.using_screen = False | |
self._screen_image = image | |
# Existing size | |
h, w, ch = image.shape | |
# Set the screen size to the original image size, not the region size | |
self.screen_width = w | |
self.screen_height = h | |
--- | |
File: /StatusParser.py | |
--- | |
import json | |
import os | |
import time | |
from datetime import datetime, timedelta | |
import queue | |
from sys import platform | |
import threading | |
from time import sleep | |
from EDAP_data import * | |
from EDlogger import logger | |
from Voice import Voice | |
from WindowsKnownPaths import * | |
class StatusParser: | |
""" Parses the Status.json file generated by the game. | |
Thanks go to Rude. See source at 'https://github.com/RatherRude/Elite-Dangerous-AI-Integration'.""" | |
def __init__(self, file_path=None): | |
if platform != "win32": | |
self.file_path = file_path if file_path else "./linux_ed/Status.json" | |
else: | |
from WindowsKnownPaths import get_path, FOLDERID, UserHandle | |
self.file_path = file_path if file_path else (get_path(FOLDERID.SavedGames, UserHandle.current) | |
+ "/Frontier Developments/Elite Dangerous/Status.json") | |
self.last_mod_time = None | |
# Read json file data | |
self.current_data = None | |
self.current_data = self.get_cleaned_data() | |
self.last_data = self.current_data | |
# self.watch_thread = threading.Thread(target=self._watch_file_thread, daemon=True) | |
# self.watch_thread.start() | |
# self.status_queue = queue.Queue() | |
# | |
# def _watch_file_thread(self): | |
# backoff = 1 | |
# while True: | |
# try: | |
# self._watch_file() | |
# except Exception as e: | |
# logger.debug('An error occurred when reading status file') | |
# sleep(backoff) | |
# logger.debug('Attempting to restart status file reader after failure') | |
# backoff *= 2 | |
# | |
# def _watch_file(self): | |
# """Detects changes in the Status.json file.""" | |
# while True: | |
# status = self.get_cleaned_data() | |
# if status != self.current_data: | |
# self.status_queue.put(status) | |
# self.current_data = status | |
# sleep(1) | |
def get_file_modified_time(self) -> float: | |
return os.path.getmtime(self.file_path) | |
def translate_flags(self, flags_value): | |
"""Translates flags integer to a dictionary of only True flags.""" | |
all_flags = { | |
"Docked": bool(flags_value & 1), | |
"Landed": bool(flags_value & 2), | |
"Landing Gear Down": bool(flags_value & 4), | |
"Shields Up": bool(flags_value & 8), | |
"Supercruise": bool(flags_value & 16), | |
"FlightAssist Off": bool(flags_value & 32), | |
"Hardpoints Deployed": bool(flags_value & 64), | |
"In Wing": bool(flags_value & 128), | |
"Lights On": bool(flags_value & 256), | |
"Cargo Scoop Deployed": bool(flags_value & 512), | |
"Silent Running": bool(flags_value & 1024), | |
"Scooping Fuel": bool(flags_value & 2048), | |
"Srv Handbrake": bool(flags_value & 4096), | |
"Srv using Turret view": bool(flags_value & 8192), | |
"Srv Turret retracted (close to ship)": bool(flags_value & 16384), | |
"Srv DriveAssist": bool(flags_value & 32768), | |
"Fsd MassLocked": bool(flags_value & 65536), | |
"Fsd Charging": bool(flags_value & 131072), | |
"Fsd Cooldown": bool(flags_value & 262144), | |
"Low Fuel (< 25%)": bool(flags_value & 524288), | |
"Over Heating (> 100%)": bool(flags_value & 1048576), | |
"Has Lat Long": bool(flags_value & 2097152), | |
"IsInDanger": bool(flags_value & 4194304), | |
"Being Interdicted": bool(flags_value & 8388608), | |
"In MainShip": bool(flags_value & 16777216), | |
"In Fighter": bool(flags_value & 33554432), | |
"In SRV": bool(flags_value & 67108864), | |
"Hud in Analysis mode": bool(flags_value & 134217728), | |
"Night Vision": bool(flags_value & 268435456), | |
"Altitude from Average radius": bool(flags_value & 536870912), | |
"Fsd Jump": bool(flags_value & 1073741824), | |
"Srv HighBeam": bool(flags_value & 2147483648), | |
} | |
# Return only flags that are True | |
true_flags = {key: value for key, value in all_flags.items() if value} | |
return true_flags | |
def translate_flags2(self, flags2_value): | |
"""Translates Flags2 integer to a dictionary of only True flags.""" | |
all_flags2 = { | |
"OnFoot": bool(flags2_value & 1), | |
"InTaxi": bool(flags2_value & 2), | |
"InMulticrew": bool(flags2_value & 4), | |
"OnFootInStation": bool(flags2_value & 8), | |
"OnFootOnPlanet": bool(flags2_value & 16), | |
"AimDownSight": bool(flags2_value & 32), | |
"LowOxygen": bool(flags2_value & 64), | |
"LowHealth": bool(flags2_value & 128), | |
"Cold": bool(flags2_value & 256), | |
"Hot": bool(flags2_value & 512), | |
"VeryCold": bool(flags2_value & 1024), | |
"VeryHot": bool(flags2_value & 2048), | |
"Glide Mode": bool(flags2_value & 4096), | |
"OnFootInHangar": bool(flags2_value & 8192), | |
"OnFootSocialSpace": bool(flags2_value & 16384), | |
"OnFootExterior": bool(flags2_value & 32768), | |
"BreathableAtmosphere": bool(flags2_value & 65536), | |
"Telepresence Multicrew": bool(flags2_value & 131072), | |
"Physical Multicrew": bool(flags2_value & 262144), | |
"Fsd hyperdrive charging": bool(flags2_value & 524288), | |
"FSD SCO Active": bool(flags2_value & 1048576), | |
"Flags2Future21": bool(flags2_value & 2097152), | |
"Flags2Future22": bool(flags2_value & 4194304), | |
"Flags2Future23": bool(flags2_value & 8388608), | |
"Flags2Future24": bool(flags2_value & 16777216), | |
"Flags2Future25": bool(flags2_value & 33554432), | |
"Flags2Future26": bool(flags2_value & 67108864), | |
"Flags2Future27": bool(flags2_value & 134217728), | |
"Flags2Future28": bool(flags2_value & 268435456), | |
"Flags2Future29": bool(flags2_value & 536870912), | |
"Flags2Future30": bool(flags2_value & 1073741824), | |
"Flags2Future31": bool(flags2_value & 2147483648), | |
} | |
# Return only flags that are True | |
true_flags2 = {key: value for key, value in all_flags2.items() if value} | |
return true_flags2 | |
def transform_pips(self, pips_list): | |
"""Transforms the pips list to a dictionary and halves each value.""" | |
return { | |
'system': pips_list[0] / 2, | |
'engine': pips_list[1] / 2, | |
'weapons': pips_list[2] / 2 | |
} | |
def adjust_year(self, timestamp): | |
"""Increases the year in the timestamp by 1286 years.""" | |
# Parse the timestamp string into a datetime object | |
dt = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%SZ") | |
# Increase the year by 1286 | |
dt = dt.replace(year=dt.year + 1286) | |
# Format the datetime object back into a string | |
return dt.strftime("%Y-%m-%dT%H:%M:%SZ") | |
def get_cleaned_data(self): | |
"""Loads data from the JSON file and returns cleaned data with only the necessary fields. | |
{ | |
"timestamp":"2024-09-28T16:01:47Z", | |
"event":"Status", | |
"Flags":150994968, | |
"Flags2":0, | |
"Pips":[4,4,4], | |
"FireGroup":0, | |
"GuiFocus":0, | |
"Fuel": | |
{ | |
"FuelMain":36.160004, | |
"FuelReservoir":0.534688 | |
}, | |
"Cargo":728.000000, | |
"LegalState":"Clean", | |
"Balance":3119756215, | |
"Destination": | |
{ | |
"System":7267487524297, | |
"Body":12, | |
"Name":"P.T.N. PERSEVERANCE TFK-N3G" | |
} | |
} | |
""" | |
# Check if file changed | |
if self.get_file_modified_time() == self.last_mod_time: | |
#logger.debug(f'Status.json mod timestamp {self.last_mod_time} unchanged.') | |
#print(f'Status.json mod timestamp {self.last_mod_time} unchanged.') | |
return self.current_data | |
# Read file | |
attempt = 1 | |
backoff = 0.1 | |
while True: | |
if os.access(self.file_path, os.R_OK): | |
try: | |
with open(self.file_path, 'r') as file: | |
data = json.load(file) | |
if attempt > 1: | |
print(f"Status file attempt: {attempt}") | |
break | |
except Exception as e: | |
logger.debug('An error occurred reading Status.json file. File may be open.') | |
sleep(backoff) | |
logger.debug('Attempting to re-read Status.json file after delay.') | |
backoff *= 2 | |
attempt = attempt + 1 | |
# Combine flags from Flags and Flags2 into a single dictionary | |
# combined_flags = {**self.translate_flags(data['Flags'])} | |
# if 'Flags2' in data: | |
# combined_flags = {**combined_flags, **self.translate_flags2(data['Flags2'])} | |
# Initialize cleaned_data with common fields | |
cleaned_data = { | |
#'status': combined_flags, | |
'time': (datetime.now() + timedelta(days=469711)).isoformat(), | |
'timestamp': datetime.strptime(data['timestamp'], "%Y-%m-%dT%H:%M:%SZ"), | |
'timestamp_delta': 0.0, # Diff in seconds from the previous timestamp in file | |
'Flags': data['Flags'], | |
'Flags2': None, | |
'Pips': None, | |
'GuiFocus': None, | |
'Cargo': None, | |
'LegalState': None, | |
'Latitude': None, | |
'Longitude': None, | |
'Heading': None, | |
'Altitude': None, | |
'PlanetRadius': None, | |
'Balance': None, | |
'Destination_System': '', | |
'Destination_Body': -1, | |
'Destination_Name': '', | |
'FuelMain': None, | |
'FuelReservoir': None, | |
} | |
# Determine the time difference between this log and the last | |
if self.current_data is not None: | |
cleaned_data['timestamp_delta'] = (cleaned_data['timestamp'] - self.current_data['timestamp']).total_seconds() | |
# Add optional status flags | |
if 'Flags2' in data: | |
cleaned_data['Flags2'] = data['Flags2'] | |
if 'Pips' in data: | |
cleaned_data['pips'] = self.transform_pips(data['Pips']) | |
if 'GuiFocus' in data: | |
cleaned_data['GuiFocus'] = data['GuiFocus'] | |
if 'Cargo' in data: | |
cleaned_data['Cargo'] = data['Cargo'] | |
if 'LegalState' in data: | |
cleaned_data['legalState'] = data['LegalState'] | |
if 'Latitude' in data: | |
cleaned_data['Latitude'] = data['Latitude'] | |
if 'Longitude' in data: | |
cleaned_data['Longitude'] = data['Longitude'] | |
if 'Heading' in data: | |
cleaned_data['Heading'] = data['Heading'] | |
if 'Altitude' in data: | |
cleaned_data['Altitude'] = data['Altitude'] | |
if 'PlanetRadius' in data: | |
cleaned_data['PlanetRadius'] = data['PlanetRadius'] | |
if 'Balance' in data: | |
cleaned_data['balance'] = data['Balance'] | |
if 'Destination' in data: | |
# Destination is the current destination, NOT the final destination. | |
cleaned_data['Destination_System'] = data['Destination']['System'] # System ID of next system. | |
cleaned_data['Destination_Body'] = data['Destination']['Body'] # Body number. '0' if the main star. | |
cleaned_data['Destination_Name'] = data['Destination']['Name'] # System, planet or station name. | |
if 'Fuel' in data: | |
cleaned_data['FuelMain'] = data['Fuel']['FuelMain'] | |
cleaned_data['FuelReservoir'] = data['Fuel']['FuelReservoir'] | |
# Store data | |
self.last_data = self.current_data | |
self.current_data = cleaned_data | |
self.last_mod_time = self.get_file_modified_time() | |
#logger.debug(f'Status.json mod timestamp {self.last_mod_time} updated.') | |
# print(f'Status.json mod timestamp {self.last_mod_time} updated.') | |
# print(json.dumps(data, indent=4)) | |
# Enable the following to print all the changes to the status flags. | |
# self.log_flag_diffs() | |
return cleaned_data | |
def log_flag_diffs(self): | |
if self.last_data is None: | |
return | |
if self.current_data is None: | |
return | |
old_flags = self.last_data['Flags'] | |
new_flags = self.current_data['Flags'] | |
flags_on = new_flags & ~ old_flags | |
flag_array = self.translate_flags(flags_on) | |
for item in flag_array: | |
print(f"Status Flags: '{item}' is ON") | |
flags_off = ~ new_flags & old_flags | |
flag_array = self.translate_flags(flags_off) | |
for item in flag_array: | |
print(f"Status Flags: '{item}' is OFF") | |
if self.last_data['Flags2'] is None: | |
return | |
if self.current_data['Flags2'] is None: | |
return | |
old_flags2 = self.last_data['Flags2'] | |
new_flags2 = self.current_data['Flags2'] | |
flags_on2 = new_flags2 & ~ old_flags2 | |
flag_array2 = self.translate_flags2(flags_on2) | |
for item in flag_array2: | |
print(f"Status Flags2: '{item}' is ON") | |
flags_off2 = ~ new_flags2 & old_flags2 | |
flag_array2 = self.translate_flags2(flags_off2) | |
for item in flag_array2: | |
print(f"Status Flags2: '{item}' is OFF") | |
def get_gui_focus(self) -> int: | |
""" Gets the value of the GUI Focus flag. | |
The Flag constants are defined in 'EDAP_data.py'. | |
""" | |
self.get_cleaned_data() | |
return self.current_data.get('GuiFocus', 0) | |
def wait_for_gui_focus(self, gui_focus_flag: int, timeout: float = 15) -> bool: | |
""" Waits for the GUI Focus flag to change to the provided value. | |
Returns True if the flag turns true or False on a time-out. | |
The Flag constants are defined in 'EDAP_data.py'. | |
@param timeout: Timeout in seconds. | |
@param gui_focus_flag: The flag to check for. | |
""" | |
start_time = time.time() | |
while (time.time() - start_time) < timeout: | |
self.get_cleaned_data() | |
if self.current_data['GuiFocus'] == gui_focus_flag: | |
return True | |
sleep(0.5) | |
return False | |
def wait_for_flag_on(self, flag: int, timeout: float = 15) -> bool: | |
""" Waits for the of the selected flag to turn true. | |
Returns True if the flag turns true or False on a time-out. | |
The Flag constants are defined in 'EDAP_data.py'. | |
@param timeout: Timeout in seconds. | |
@param flag: The flag to check for. | |
""" | |
start_time = time.time() | |
while (time.time() - start_time) < timeout: | |
self.get_cleaned_data() | |
if bool(self.current_data['Flags'] & flag): | |
return True | |
sleep(0.5) | |
return False | |
def wait_for_flag_off(self, flag: int, timeout: float = 15) -> bool: | |
""" Waits for the of the selected flag to turn false. | |
Returns True if the flag turns false or False on a time-out. | |
The Flag constants are defined in 'EDAP_data.py'. | |
@param timeout: Timeout in seconds. | |
@param flag: The flag to check for. | |
""" | |
start_time = time.time() | |
while (time.time() - start_time) < timeout: | |
self.get_cleaned_data() | |
if not bool(self.current_data['Flags'] & flag): | |
return True | |
sleep(0.5) | |
return False | |
def wait_for_flag2_on(self, flag: int, timeout: float = 15) -> bool: | |
""" Waits for the of the selected flag to turn true. | |
Returns True if the flag turns true or False on a time-out. | |
The Flag constants are defined in 'EDAP_data.py'. | |
@param timeout: Timeout in seconds. | |
@param flag: The flag to check for. | |
""" | |
if 'Flags2' not in self.current_data: | |
return False | |
start_time = time.time() | |
while (time.time() - start_time) < timeout: | |
self.get_cleaned_data() | |
if bool(self.current_data['Flags2'] & flag): | |
return True | |
sleep(0.5) | |
return False | |
def wait_for_flag2_off(self, flag: int, timeout: float = 15) -> bool: | |
""" Waits for the of the selected flag to turn false. | |
Returns True if the flag turns false or False on a time-out. | |
The Flag constants are defined in 'EDAP_data.py'. | |
@param timeout: Timeout in seconds. | |
@param flag: The flag to check for. | |
""" | |
if 'Flags2' not in self.current_data: | |
return False | |
start_time = time.time() | |
while (time.time() - start_time) < timeout: | |
self.get_cleaned_data() | |
if not bool(self.current_data['Flags2'] & flag): | |
return True | |
sleep(0.5) | |
return False | |
def get_flag(self, flag: int) -> bool: | |
""" Gets the value of the selected flag. | |
The Flag constants are defined in 'EDAP_data.py'. | |
@param flag: The flag to check for. | |
""" | |
self.get_cleaned_data() | |
return bool(self.current_data['Flags'] & flag) | |
def get_flag2(self, flag: int) -> bool: | |
""" Gets the value of the selected flag2. | |
The Flag2 constants are defined in 'EDAP_data.py'. | |
@param flag: The flag to check for. | |
""" | |
self.get_cleaned_data() | |
if 'Flags2' in self.current_data: | |
if self.current_data['Flags2'] is not None: | |
return bool(self.current_data['Flags2'] & flag) | |
return False | |
else: | |
return False | |
def wait_for_file_change(self, start_timestamp, timeout: float = 5) -> bool: | |
""" Waits for the file to change. | |
Returns True if the file changes or False on a time-out. | |
@param start_timestamp: The initial timestamp from 'timestamp' value. | |
@param timeout: Timeout in seconds. | |
""" | |
start_time = time.time() | |
while (time.time() - start_time) < timeout: | |
# Check file and read now data | |
self.get_cleaned_data() | |
# Check if internal timestamp changed | |
if self.current_data['timestamp'] != start_timestamp: | |
return True | |
sleep(0.5) | |
return False | |
# Usage Example | |
if __name__ == "__main__": | |
parser = StatusParser() | |
while True: | |
parser.get_cleaned_data() | |
parser.log_flag_diffs() | |
parser.last_data = parser.current_data | |
sleep(1) | |
--- | |
File: /Test_Routines.py | |
--- | |
import logging | |
import cv2 | |
import os | |
from ED_AP import EDAutopilot | |
from Screen_Regions import * | |
from Overlay import * | |
from Screen import * | |
from Image_Templates import * | |
from time import sleep | |
import numpy as np | |
""" | |
File:Test_Routines.py | |
Description: | |
Class to allow testing | |
""" | |
def main(): | |
# Uncomment one tests to be performed as each runs in a loop until exited. | |
# Run this file instead of the main EDAPGUI file. | |
logger.setLevel(logging.DEBUG) # Default to log all debug when running this file. | |
# Rescale screenshots from the user scaling (i.e. 1920x1080 [0.75,0.75] | |
# to the default scaling of 3440x1440 [1.0, 1.0]. Note the scaling is by | |
# the ratio, not the resolution. Refer to Screen.py for resolution to | |
# ratio conversions. | |
# This only needs to be competed for new screenshots that were not take | |
# at 3440x1440. Once complete, remove the original images and move the | |
# converted images to relevant test folder. | |
# | |
# Does NOT require Elite Dangerous to be running. | |
# ====================================================================== | |
# rescale_screenshots('test/images-to-rescale', 0.76, 0.76) | |
# Shows filtering and matching for the specified region... | |
# Requires Elite Dangerous to be running. | |
# ======================================================== | |
# template_matching_test('compass', 'compass') | |
# template_matching_test('compass','navpoint') | |
# template_matching_test('target', 'target') | |
template_matching_test('target_occluded', 'target_occluded') | |
# More complicated specific test cases... | |
# ======================================= | |
# Requires Elite Dangerous to be running. | |
# compass_test() | |
# Shows regions on the Elite window... | |
# Requires Elite Dangerous to be running. | |
# ======================================= | |
#wanted_regions = ["compass", "target", "nav_panel", "disengage", "fss", "mission_dest", "missions", "sun"] | |
# wanted_regions = ["compass", "target", "nav_panel", "disengage"] # The more common regions for navigation | |
#show_regions(wanted_regions) | |
# HSV Tester... | |
# | |
# Does NOT require Elite Dangerous to be running. | |
# =============================================== | |
# hsv_tester("test/compass/Screenshot 2024-07-04 20-01-49.png") | |
# hsv_tester("test/disengage/Screenshot 2024-08-13 21-32-58.png") | |
# hsv_tester("test/navpoint/Screenshot 2024-07-04 20-02-01.png") | |
# hsv_tester("test/navpoint-behind/Screenshot 2024-07-04 20-01-33.png") | |
# hsv_tester("test/target/Screenshot 2024-07-04 23-22-02.png") | |
# hsv_tester("test/target-occluded/Screenshot 2025-05-04 16-07-09_cr.png") | |
def draw_match_rect(img, pt1, pt2, color, thick): | |
""" Utility function to add a rectangle to an image. """ | |
wid = pt2[0] - pt1[0] | |
hgt = pt2[1] - pt1[1] | |
if wid < 20: | |
# cv2.rectangle(screen, pt, (pt[0] + compass_width, pt[1] + compass_height), (0,0,255), 2) | |
cv2.rectangle(img, pt1, pt2, color, thick) | |
else: | |
len_wid = wid / 5 | |
len_hgt = hgt / 5 | |
half_wid = wid / 2 | |
half_hgt = hgt / 2 | |
tic_len = thick - 1 | |
# top | |
cv2.line(img, (int(pt1[0]), int(pt1[1])), (int(pt1[0] + len_wid), int(pt1[1])), color, thick) | |
cv2.line(img, (int(pt1[0] + (2 * len_wid)), int(pt1[1])), (int(pt1[0] + (3 * len_wid)), int(pt1[1])), color, 1) | |
cv2.line(img, (int(pt1[0] + (4 * len_wid)), int(pt1[1])), (int(pt2[0]), int(pt1[1])), color, thick) | |
# top tic | |
cv2.line(img, (int(pt1[0] + half_wid), int(pt1[1])), (int(pt1[0] + half_wid), int(pt1[1]) - tic_len), color, | |
thick) | |
# bot | |
cv2.line(img, (int(pt1[0]), int(pt2[1])), (int(pt1[0] + len_wid), int(pt2[1])), color, thick) | |
cv2.line(img, (int(pt1[0] + (2 * len_wid)), int(pt2[1])), (int(pt1[0] + (3 * len_wid)), int(pt2[1])), color, 1) | |
cv2.line(img, (int(pt1[0] + (4 * len_wid)), int(pt2[1])), (int(pt2[0]), int(pt2[1])), color, thick) | |
# bot tic | |
cv2.line(img, (int(pt1[0] + half_wid), int(pt2[1])), (int(pt1[0] + half_wid), int(pt2[1]) + tic_len), color, | |
thick) | |
# left | |
cv2.line(img, (int(pt1[0]), int(pt1[1])), (int(pt1[0]), int(pt1[1] + len_hgt)), color, thick) | |
cv2.line(img, (int(pt1[0]), int(pt1[1] + (2 * len_hgt))), (int(pt1[0]), int(pt1[1] + (3 * len_hgt))), color, 1) | |
cv2.line(img, (int(pt1[0]), int(pt1[1] + (4 * len_hgt))), (int(pt1[0]), int(pt2[1])), color, thick) | |
# left tic | |
cv2.line(img, (int(pt1[0]), int(pt1[1] + half_hgt)), (int(pt1[0] - tic_len), int(pt1[1] + half_hgt)), color, | |
thick) | |
# right | |
cv2.line(img, (int(pt2[0]), int(pt1[1])), (int(pt2[0]), int(pt1[1] + len_hgt)), color, thick) | |
cv2.line(img, (int(pt2[0]), int(pt1[1] + (2 * len_hgt))), (int(pt2[0]), int(pt1[1] + (3 * len_hgt))), color, 1) | |
cv2.line(img, (int(pt2[0]), int(pt1[1] + (4 * len_hgt))), (int(pt2[0]), int(pt2[1])), color, thick) | |
# right tic | |
cv2.line(img, (int(pt2[0]), int(pt1[1] + half_hgt)), (int(pt2[0] + tic_len), int(pt1[1] + half_hgt)), color, | |
thick) | |
def compass_test(): | |
""" Performs a compass test. """ | |
ed_ap = EDAutopilot(cb=None) | |
scr = ed_ap.scr | |
templ = Image_Templates(scr.scaleX, scr.scaleY, ed_ap.compass_scale) | |
scr_reg = Screen_Regions(scr, templ) | |
while True: | |
region_name = 'compass' | |
template = 'compass' | |
img_region, (minVal, maxVal, minLoc, maxLoc), match = scr_reg.match_template_in_region(region_name, template) | |
pt = maxLoc | |
c_wid = scr_reg.templates.template['compass']['width'] | |
c_hgt = scr_reg.templates.template['compass']['height'] | |
draw_match_rect(img_region, pt, (pt[0] + c_wid, pt[1] + c_hgt), (0, 0, 255), 2) | |
cv2.putText(img_region, f'Match: {maxVal:5.2f}', (1, 10), cv2.FONT_HERSHEY_SIMPLEX, 0.35, | |
(255, 255, 255), 1, cv2.LINE_AA) | |
cv2.imshow(region_name, img_region) | |
cv2.imshow(template + ' match', match) | |
key = cv2.waitKey(1) | |
if key == 27: # ESC | |
break | |
def template_matching_test(region_name, template): | |
""" To test the template matching. Using the provided region and template. | |
:param region_name: The name of the region with the required filter to apply to the image. | |
:param template: The name of the template to find in each file being tested. """ | |
#ed_ap = EDAutopilot(cb=None) | |
#scr = ed_ap.scr | |
scr = Screen(cb=None) | |
templ = Image_Templates(scr.scaleX, scr.scaleY, scr.scaleX) | |
scr_reg = Screen_Regions(scr, templ) | |
while True: | |
img_region, (minVal, maxVal, minLoc, maxLoc), match = scr_reg.match_template_in_region(region_name, template) | |
cv2.putText(img_region, f'Match: {maxVal:5.2f}', (1, 10), cv2.FONT_HERSHEY_SIMPLEX, 0.35, | |
(255, 255, 255), 1, cv2.LINE_AA) | |
cv2.imshow(region_name, img_region) | |
cv2.imshow(template + ' match', match) | |
key = cv2.waitKey(10) | |
if key == 27: # ESC | |
break | |
def show_regions(region_names): | |
""" Draw a rectangle indicating the given region on the Elite Dangerous window. | |
:param region_names: An array names of the regions to indicate on screen (i.e. ["compass", "target"]).""" | |
ed_ap = EDAutopilot(cb=None) | |
scr = ed_ap.scr | |
ov = ed_ap.overlay | |
templ = Image_Templates(scr.scaleX, scr.scaleY, scr.scaleX) | |
scrReg = Screen_Regions(scr, templ) | |
overlay_colors = [ | |
(255, 255, 255), | |
(255, 0, 0), | |
(0, 255, 0), | |
(0, 0, 255), | |
(255, 255, 0), | |
(0, 255, 255), | |
(255, 0, 255), | |
(192, 192, 192), | |
(128, 128, 128), | |
(128, 0, 0), | |
(128, 128, 0), | |
(0, 128, 0), | |
(128, 0, 128), | |
(0, 128, 128), | |
(0, 0, 128) | |
] | |
for i, key in enumerate(scrReg.reg): | |
#tgt = scrReg.capture_region_filtered(scr, key) | |
#print(key) | |
#print(scrReg.reg[key]) | |
if key in region_names: | |
ov.overlay_rect(key, (scrReg.reg[key]['rect'][0], scrReg.reg[key]['rect'][1]), | |
(scrReg.reg[key]['rect'][2], scrReg.reg[key]['rect'][3]), | |
overlay_colors[i+1], 2) | |
ov.overlay_floating_text(key, key, scrReg.reg[key]['rect'][0], scrReg.reg[key]['rect'][1], | |
overlay_colors[i+1]) | |
ov.overlay_paint() | |
sleep(10) | |
ov.overlay_quit() | |
sleep(2) | |
def hsv_tester(image_path): | |
""" Brings up a HSV test window with sliders to check the 'inRange' function on the provided image. | |
Change the default values below where indicated to the values associated with the appropriate | |
template in image_template.py. | |
:param image_path: The file path of the image to test. """ | |
cv2.namedWindow("Trackbars", cv2.WINDOW_NORMAL) # cv2.WINDOW_AUTOSIZE) | |
cv2.createTrackbar("L - H", "Trackbars", 0, 179, callback) | |
cv2.createTrackbar("L - S", "Trackbars", 0, 255, callback) | |
cv2.createTrackbar("L - V", "Trackbars", 0, 255, callback) | |
cv2.createTrackbar("U - H", "Trackbars", 255, 179, callback) | |
cv2.createTrackbar("U - S", "Trackbars", 255, 255, callback) | |
cv2.createTrackbar("U - V", "Trackbars", 255, 255, callback) | |
frame = cv2.imread(image_path) | |
# Set default values | |
cv2.setTrackbarPos("L - H", "Trackbars", 43) | |
cv2.setTrackbarPos("L - S", "Trackbars", 35) | |
cv2.setTrackbarPos("L - V", "Trackbars", 100) | |
cv2.setTrackbarPos("U - H", "Trackbars", 100) | |
cv2.setTrackbarPos("U - S", "Trackbars", 255) | |
cv2.setTrackbarPos("U - V", "Trackbars", 255) | |
while True: | |
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) | |
l_h = cv2.getTrackbarPos("L - H", "Trackbars") | |
l_s = cv2.getTrackbarPos("L - S", "Trackbars") | |
l_v = cv2.getTrackbarPos("L - V", "Trackbars") | |
u_h = cv2.getTrackbarPos("U - H", "Trackbars") | |
u_s = cv2.getTrackbarPos("U - S", "Trackbars") | |
u_v = cv2.getTrackbarPos("U - V", "Trackbars") | |
lower_range = np.array([l_h, l_s, l_v]) | |
upper_range = np.array([u_h, u_s, u_v]) | |
mask = cv2.inRange(hsv, lower_range, upper_range) | |
result = cv2.bitwise_and(frame, frame, mask=mask) | |
cv2.imshow("original", frame) | |
cv2.imshow("mask", mask) | |
cv2.imshow("result", result) | |
key = cv2.waitKey(1) | |
if key == 27: # ESC | |
break | |
cv2.destroyAllWindows() | |
def rescale_screenshots(directory, scalex, scaley): | |
""" Rescale all images in a folder. Also convert BMP to PNG | |
:param directory: The directory to process. | |
:param scalex: The X scaling of the original image. | |
:param scaley: The scaling of the original image. """ | |
# Calc factor to scale image up/down | |
newScaleX = 1.0 / scalex | |
newScaleY = 1.0 / scaley | |
directory_out = os.path.join(directory, 'out') | |
if not os.path.exists(directory_out): | |
os.makedirs(directory_out) | |
for filename in os.listdir(directory): | |
if filename.endswith(".png") or filename.endswith(".bmp"): | |
image_path = os.path.join(directory, filename) | |
image_out_path = os.path.join(directory_out, filename.replace('bmp', 'png')) | |
image = cv2.imread(image_path) | |
# Scale image to user scaling | |
image = cv2.resize(image, (0, 0), fx=newScaleX, fy=newScaleY) | |
cv2.imwrite(image_out_path, image) | |
def callback(value): | |
print(value) | |
if __name__ == "__main__": | |
main() | |
--- | |
File: /Voice.py | |
--- | |
# pip install pyttsx3 | |
from threading import Thread | |
import kthread | |
import queue | |
import pyttsx3 | |
from time import sleep | |
#rate = voiceEngine.getProperty('rate') | |
#volume = voiceEngine.getProperty('volume') | |
#voice = voiceEngine.getProperty('voice') | |
#voiceEngine.setProperty('rate', newVoiceRate) | |
#voiceEngine.setProperty('voice', voice.id) id = 0, 1, ... | |
""" | |
File:Voice.py | |
Description: | |
Class to enapsulate the Text to Speech package in python | |
To Use: | |
See main() at bottom as example | |
Author: [email protected] | |
""" | |
class Voice: | |
def __init__(self): | |
self.q = queue.Queue(5) | |
self.v_enabled = False | |
self.v_quit = False | |
self.t = kthread.KThread(target=self.voice_exec, name="Voice", daemon=True) | |
self.t.start() | |
self.v_id = 1 | |
def say(self, vSay): | |
if self.v_enabled: | |
# A better way to correct mis-pronunciation? | |
vSay = vSay.replace(' Mk V ', ' mark five ') | |
vSay = vSay.replace(' Mk ', ' mark ') | |
vSay = vSay.replace(' Krait ', ' crate ') | |
self.q.put(vSay) | |
def set_off(self): | |
self.v_enabled = False | |
def set_on(self): | |
self.v_enabled = True | |
def set_voice_id(self, id): | |
self.v_id = id | |
def quit(self): | |
self.v_quit = True | |
def voice_exec(self): | |
engine = pyttsx3.init() | |
voices = engine.getProperty('voices') | |
v_id_current = 0 # David | |
engine.setProperty('voice', voices[v_id_current].id) | |
engine.setProperty('rate', 160) | |
while not self.v_quit: | |
# check if the voice ID changed | |
if self.v_id != v_id_current: | |
v_id_current = self.v_id | |
try: | |
engine.setProperty('voice', voices[v_id_current].id) | |
except: | |
print("Voice ID out of range") | |
try: | |
words = self.q.get(timeout=1) | |
self.q.task_done() | |
if words is not None: | |
engine.say(words) | |
engine.runAndWait() | |
except: | |
pass | |
def main(): | |
v = Voice() | |
v.set_on() | |
sleep(2) | |
v.say("Hey dude") | |
sleep(2) | |
v.say("whats up") | |
sleep(2) | |
v.quit() | |
if __name__ == "__main__": | |
main() | |
--- | |
File: /WindowsKnownPaths.py | |
--- | |
import ctypes, sys | |
from ctypes import windll, wintypes | |
from uuid import UUID | |
class GUID(ctypes.Structure): # [1] | |
_fields_ = [ | |
("Data1", wintypes.DWORD), | |
("Data2", wintypes.WORD), | |
("Data3", wintypes.WORD), | |
("Data4", wintypes.BYTE * 8) | |
] | |
def __init__(self, uuid_): | |
ctypes.Structure.__init__(self) | |
self.Data1, self.Data2, self.Data3, self.Data4[0], self.Data4[1], rest = uuid_.fields | |
for i in range(2, 8): | |
self.Data4[i] = rest >> (8 - i - 1)*8 & 0xff | |
class FOLDERID: # [2] | |
AccountPictures = UUID('{008ca0b1-55b4-4c56-b8a8-4de4b299d3be}') | |
AdminTools = UUID('{724EF170-A42D-4FEF-9F26-B60E846FBA4F}') | |
ApplicationShortcuts = UUID('{A3918781-E5F2-4890-B3D9-A7E54332328C}') | |
CameraRoll = UUID('{AB5FB87B-7CE2-4F83-915D-550846C9537B}') | |
CDBurning = UUID('{9E52AB10-F80D-49DF-ACB8-4330F5687855}') | |
CommonAdminTools = UUID('{D0384E7D-BAC3-4797-8F14-CBA229B392B5}') | |
CommonOEMLinks = UUID('{C1BAE2D0-10DF-4334-BEDD-7AA20B227A9D}') | |
CommonPrograms = UUID('{0139D44E-6AFE-49F2-8690-3DAFCAE6FFB8}') | |
CommonStartMenu = UUID('{A4115719-D62E-491D-AA7C-E74B8BE3B067}') | |
CommonStartup = UUID('{82A5EA35-D9CD-47C5-9629-E15D2F714E6E}') | |
CommonTemplates = UUID('{B94237E7-57AC-4347-9151-B08C6C32D1F7}') | |
Contacts = UUID('{56784854-C6CB-462b-8169-88E350ACB882}') | |
Cookies = UUID('{2B0F765D-C0E9-4171-908E-08A611B84FF6}') | |
Desktop = UUID('{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}') | |
DeviceMetadataStore = UUID('{5CE4A5E9-E4EB-479D-B89F-130C02886155}') | |
Documents = UUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}') | |
DocumentsLibrary = UUID('{7B0DB17D-9CD2-4A93-9733-46CC89022E7C}') | |
Downloads = UUID('{374DE290-123F-4565-9164-39C4925E467B}') | |
Favorites = UUID('{1777F761-68AD-4D8A-87BD-30B759FA33DD}') | |
Fonts = UUID('{FD228CB7-AE11-4AE3-864C-16F3910AB8FE}') | |
GameTasks = UUID('{054FAE61-4DD8-4787-80B6-090220C4B700}') | |
History = UUID('{D9DC8A3B-B784-432E-A781-5A1130A75963}') | |
ImplicitAppShortcuts = UUID('{BCB5256F-79F6-4CEE-B725-DC34E402FD46}') | |
InternetCache = UUID('{352481E8-33BE-4251-BA85-6007CAEDCF9D}') | |
Libraries = UUID('{1B3EA5DC-B587-4786-B4EF-BD1DC332AEAE}') | |
Links = UUID('{bfb9d5e0-c6a9-404c-b2b2-ae6db6af4968}') | |
LocalAppData = UUID('{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}') | |
LocalAppDataLow = UUID('{A520A1A4-1780-4FF6-BD18-167343C5AF16}') | |
LocalizedResourcesDir = UUID('{2A00375E-224C-49DE-B8D1-440DF7EF3DDC}') | |
Music = UUID('{4BD8D571-6D19-48D3-BE97-422220080E43}') | |
MusicLibrary = UUID('{2112AB0A-C86A-4FFE-A368-0DE96E47012E}') | |
NetHood = UUID('{C5ABBF53-E17F-4121-8900-86626FC2C973}') | |
OriginalImages = UUID('{2C36C0AA-5812-4b87-BFD0-4CD0DFB19B39}') | |
PhotoAlbums = UUID('{69D2CF90-FC33-4FB7-9A0C-EBB0F0FCB43C}') | |
PicturesLibrary = UUID('{A990AE9F-A03B-4E80-94BC-9912D7504104}') | |
Pictures = UUID('{33E28130-4E1E-4676-835A-98395C3BC3BB}') | |
Playlists = UUID('{DE92C1C7-837F-4F69-A3BB-86E631204A23}') | |
PrintHood = UUID('{9274BD8D-CFD1-41C3-B35E-B13F55A758F4}') | |
Profile = UUID('{5E6C858F-0E22-4760-9AFE-EA3317B67173}') | |
ProgramData = UUID('{62AB5D82-FDC1-4DC3-A9DD-070D1D495D97}') | |
ProgramFiles = UUID('{905e63b6-c1bf-494e-b29c-65b732d3d21a}') | |
ProgramFilesX64 = UUID('{6D809377-6AF0-444b-8957-A3773F02200E}') | |
ProgramFilesX86 = UUID('{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}') | |
ProgramFilesCommon = UUID('{F7F1ED05-9F6D-47A2-AAAE-29D317C6F066}') | |
ProgramFilesCommonX64 = UUID('{6365D5A7-0F0D-45E5-87F6-0DA56B6A4F7D}') | |
ProgramFilesCommonX86 = UUID('{DE974D24-D9C6-4D3E-BF91-F4455120B917}') | |
Programs = UUID('{A77F5D77-2E2B-44C3-A6A2-ABA601054A51}') | |
Public = UUID('{DFDF76A2-C82A-4D63-906A-5644AC457385}') | |
PublicDesktop = UUID('{C4AA340D-F20F-4863-AFEF-F87EF2E6BA25}') | |
PublicDocuments = UUID('{ED4824AF-DCE4-45A8-81E2-FC7965083634}') | |
PublicDownloads = UUID('{3D644C9B-1FB8-4f30-9B45-F670235F79C0}') | |
PublicGameTasks = UUID('{DEBF2536-E1A8-4c59-B6A2-414586476AEA}') | |
PublicLibraries = UUID('{48DAF80B-E6CF-4F4E-B800-0E69D84EE384}') | |
PublicMusic = UUID('{3214FAB5-9757-4298-BB61-92A9DEAA44FF}') | |
PublicPictures = UUID('{B6EBFB86-6907-413C-9AF7-4FC2ABF07CC5}') | |
PublicRingtones = UUID('{E555AB60-153B-4D17-9F04-A5FE99FC15EC}') | |
PublicUserTiles = UUID('{0482af6c-08f1-4c34-8c90-e17ec98b1e17}') | |
PublicVideos = UUID('{2400183A-6185-49FB-A2D8-4A392A602BA3}') | |
QuickLaunch = UUID('{52a4f021-7b75-48a9-9f6b-4b87a210bc8f}') | |
Recent = UUID('{AE50C081-EBD2-438A-8655-8A092E34987A}') | |
RecordedTVLibrary = UUID('{1A6FDBA2-F42D-4358-A798-B74D745926C5}') | |
ResourceDir = UUID('{8AD10C31-2ADB-4296-A8F7-E4701232C972}') | |
Ringtones = UUID('{C870044B-F49E-4126-A9C3-B52A1FF411E8}') | |
RoamingAppData = UUID('{3EB685DB-65F9-4CF6-A03A-E3EF65729F3D}') | |
RoamedTileImages = UUID('{AAA8D5A5-F1D6-4259-BAA8-78E7EF60835E}') | |
RoamingTiles = UUID('{00BCFC5A-ED94-4e48-96A1-3F6217F21990}') | |
SampleMusic = UUID('{B250C668-F57D-4EE1-A63C-290EE7D1AA1F}') | |
SamplePictures = UUID('{C4900540-2379-4C75-844B-64E6FAF8716B}') | |
SamplePlaylists = UUID('{15CA69B3-30EE-49C1-ACE1-6B5EC372AFB5}') | |
SampleVideos = UUID('{859EAD94-2E85-48AD-A71A-0969CB56A6CD}') | |
SavedGames = UUID('{4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4}') | |
SavedSearches = UUID('{7d1d3a04-debb-4115-95cf-2f29da2920da}') | |
Screenshots = UUID('{b7bede81-df94-4682-a7d8-57a52620b86f}') | |
SearchHistory = UUID('{0D4C3DB6-03A3-462F-A0E6-08924C41B5D4}') | |
SearchTemplates = UUID('{7E636BFE-DFA9-4D5E-B456-D7B39851D8A9}') | |
SendTo = UUID('{8983036C-27C0-404B-8F08-102D10DCFD74}') | |
SidebarDefaultParts = UUID('{7B396E54-9EC5-4300-BE0A-2482EBAE1A26}') | |
SidebarParts = UUID('{A75D362E-50FC-4fb7-AC2C-A8BEAA314493}') | |
SkyDrive = UUID('{A52BBA46-E9E1-435f-B3D9-28DAA648C0F6}') | |
SkyDriveCameraRoll = UUID('{767E6811-49CB-4273-87C2-20F355E1085B}') | |
SkyDriveDocuments = UUID('{24D89E24-2F19-4534-9DDE-6A6671FBB8FE}') | |
SkyDrivePictures = UUID('{339719B5-8C47-4894-94C2-D8F77ADD44A6}') | |
StartMenu = UUID('{625B53C3-AB48-4EC1-BA1F-A1EF4146FC19}') | |
Startup = UUID('{B97D20BB-F46A-4C97-BA10-5E3608430854}') | |
System = UUID('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}') | |
SystemX86 = UUID('{D65231B0-B2F1-4857-A4CE-A8E7C6EA7D27}') | |
Templates = UUID('{A63293E8-664E-48DB-A079-DF759E0509F7}') | |
UserPinned = UUID('{9E3995AB-1F9C-4F13-B827-48B24B6C7174}') | |
UserProfiles = UUID('{0762D272-C50A-4BB0-A382-697DCD729B80}') | |
UserProgramFiles = UUID('{5CD7AEE2-2219-4A67-B85D-6C9CE15660CB}') | |
UserProgramFilesCommon = UUID('{BCBD3057-CA5C-4622-B42D-BC56DB0AE516}') | |
Videos = UUID('{18989B1D-99B5-455B-841C-AB7C74E4DDFC}') | |
VideosLibrary = UUID('{491E922F-5643-4AF4-A7EB-4E7A138D8174}') | |
Windows = UUID('{F38BF404-1D43-42F2-9305-67DE0B28FC23}') | |
class UserHandle: # [3] | |
current = wintypes.HANDLE(0) | |
common = wintypes.HANDLE(-1) | |
_CoTaskMemFree = windll.ole32.CoTaskMemFree # [4] | |
_CoTaskMemFree.restype= None | |
_CoTaskMemFree.argtypes = [ctypes.c_void_p] | |
_SHGetKnownFolderPath = windll.shell32.SHGetKnownFolderPath # [5] [3] | |
_SHGetKnownFolderPath.argtypes = [ | |
ctypes.POINTER(GUID), wintypes.DWORD, wintypes.HANDLE, ctypes.POINTER(ctypes.c_wchar_p) | |
] | |
class PathNotFoundException(Exception): pass | |
def get_path(folderid, user_handle=UserHandle.common): | |
fid = GUID(folderid) | |
pPath = ctypes.c_wchar_p() | |
S_OK = 0 | |
if _SHGetKnownFolderPath(ctypes.byref(fid), 0, user_handle, ctypes.byref(pPath)) != S_OK: | |
raise PathNotFoundException() | |
path = pPath.value | |
_CoTaskMemFree(pPath) | |
return path | |
if __name__ == '__main__': | |
if len(sys.argv) < 2 or sys.argv[1] in ['-?', '/?']: | |
print('python knownpaths.py FOLDERID {current|common}') | |
sys.exit(0) | |
try: | |
folderid = getattr(FOLDERID, sys.argv[1]) | |
except AttributeError: | |
print('Unknown folder id "%s"' % sys.argv[1], file=sys.stderr) | |
sys.exit(1) | |
try: | |
if len(sys.argv) == 2: | |
print(get_path(folderid)) | |
else: | |
print(get_path(folderid, getattr(UserHandle, sys.argv[2]))) | |
except PathNotFoundException: | |
print('Folder not found "%s"' % ' '.join(sys.argv[1:]), file=sys.stderr) | |
sys.exit(1) | |
# [1] http://msdn.microsoft.com/en-us/library/windows/desktop/aa373931.aspx | |
# [2] http://msdn.microsoft.com/en-us/library/windows/desktop/dd378457.aspx | |
# [3] http://msdn.microsoft.com/en-us/library/windows/desktop/bb762188.aspx | |
# [4] http://msdn.microsoft.com/en-us/library/windows/desktop/ms680722.aspx | |
# [5] http://www.themacaque.com/?p=954 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment