Skip to content

Instantly share code, notes, and snippets.

@qoli
Created June 6, 2025 05:48
Show Gist options
  • Save qoli/ffd9b461f7224e5c144add943e450f3c to your computer and use it in GitHub Desktop.
Save qoli/ffd9b461f7224e5c144add943e450f3c to your computer and use it in GitHub Desktop.
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