diff --git a/App.py b/App.py index 1623b1b..fc87fc9 100644 --- a/App.py +++ b/App.py @@ -1,11 +1,12 @@ import time -from src.GamePackage import Player, Script, Map, PathFinder +from src.GamePackage import Player, Script, Map, PathFinder, TradeScript from src.LoggerPackage import Logger from src.OperatingSystemPackage import Kernel, Monitor, GlobalGameWidgetContainer, Keyboard, Mouse from src.VendorPackage import PyAutoGui, TesseractOcr from src.Cavebot import CaveBot from src.Train import AutoTrainer +from src.Trader import AutoTrader from src.TaskPackage import TaskResolver from src.SharedPackage import Constants, GameContext @@ -28,7 +29,7 @@ def init(self) -> None: game_context = GameContext() - if Constants.TRAIN_MODE not in os.environ: + if Constants.TRAIN_MODE not in os.environ and Constants.TRADE_MODE not in os.environ: game_context.set_script_enemies(self.__script.creatures()) game_context.set_cave_route(self.__script.waypoints()) game_context.set_has_to_wear_ring(self.__script.has_to_wear_ring()) @@ -49,14 +50,31 @@ def init(self) -> None: return - auto_trainer = AutoTrainer( + if Constants.TRAIN_MODE in os.environ: + auto_trainer = AutoTrainer( + self.__monitor, + self.__task_resolver, + self.__global_widget_container, + self.__tesseract + ) + + auto_trainer.start(game_context, player) + + return + + script = TradeScript.load('src/Wiki/Script/Trade/trade.json') + + game_context.set_trade_items(script.items()) + + auto_trader = AutoTrader( self.__monitor, self.__task_resolver, self.__global_widget_container, - self.__tesseract + self.__pyautogui ) - auto_trainer.start(game_context, player) + auto_trader.start(game_context, player) + except KeyboardInterrupt: Logger.info('Graceful shutdown') @@ -65,7 +83,7 @@ def init(self) -> None: Logger.error(str(error), error) Logger.info('Force character logout') - self.__kernel.force_game_logout() + # self.__kernel.force_game_logout() raise SystemExit from error @@ -148,6 +166,12 @@ def __collect_program_arguments(self) -> None: help='Indicate that program should start in training mode' ) + parser.add_argument( + '--trade', + action="store_true", + help='Indicate that program should start in trade mode' + ) + if parser.parse_args(): table = Table() table.add_column("Argument", justify="left", style="magenta", no_wrap=True) @@ -157,6 +181,10 @@ def __collect_program_arguments(self) -> None: os.environ[Constants.TRAIN_MODE] = Constants.TRAIN_MODE table.add_row("--train", Constants.TRAIN_MODE) + if parser.parse_args().trade: + os.environ[Constants.TRADE_MODE] = Constants.TRADE_MODE + table.add_row("--trade", Constants.TRADE_MODE) + if parser.parse_args().debug: os.environ[Constants.DEBUG_MODE] = Constants.DEBUG_MODE table.add_row("--debug", Constants.DEBUG_MODE) @@ -165,7 +193,7 @@ def __collect_program_arguments(self) -> None: os.environ[Constants.DEV_MODE] = Constants.DEV_MODE table.add_row("--dev", Constants.DEV_MODE) - if not parser.parse_args().dev and not parser.parse_args().train and not parser.parse_args().debug: + if not parser.parse_args().dev and not parser.parse_args().train and not parser.parse_args().debug and not parser.parse_args().trade: os.environ[Constants.PRODUCTION_MODE] = Constants.PRODUCTION_MODE table.add_row("production", Constants.PRODUCTION_MODE) diff --git a/src/GamePackage/Player.py b/src/GamePackage/Player.py index baf96b5..014f85c 100644 --- a/src/GamePackage/Player.py +++ b/src/GamePackage/Player.py @@ -1,3 +1,5 @@ +from time import sleep + from src.LoggerPackage import Logger from src.OperatingSystemPackage import Keyboard, Mouse from src.SharedPackage import Creature, Coordinate, MoveCommand @@ -67,3 +69,20 @@ def chase_opponent(self) -> None: def use_stealth_ring(self) -> None: Logger.info('Equip stealth ring') self.__keyboard.press('t') + + def open(self, coordinates: Coordinate) -> None: + self.__mouse.use_right_button(coordinates) + + def left_click(self, coordinates: Coordinate) -> None: + self.__mouse.use_left_button(coordinates) + + def write(self, word: str) -> None: + letters = list(word) + + for letter in letters: + if letter == " ": + self.__keyboard.press("space") + continue + + self.__keyboard.press(letter) + sleep(0.2) diff --git a/src/GamePackage/TradeScript.py b/src/GamePackage/TradeScript.py new file mode 100644 index 0000000..d31fc41 --- /dev/null +++ b/src/GamePackage/TradeScript.py @@ -0,0 +1,23 @@ +import json +from src.UtilPackage import LinkedList + +class TradeScript: + __FILE_READ_MODE = 'r' + + __items: LinkedList = LinkedList[str]() + + + def __init__(self, script_json_data: dict): + for item in script_json_data['item']: + self.__items.append(item) + + + @staticmethod + def load(name: str) -> 'TradeScript': + with open(name, TradeScript.__FILE_READ_MODE) as file: + script_data = json.load(file) + + return TradeScript(script_data) + + def items(self) -> LinkedList[str]: + return self.__items diff --git a/src/GamePackage/__init__.py b/src/GamePackage/__init__.py index 24ec18b..d8adfd4 100644 --- a/src/GamePackage/__init__.py +++ b/src/GamePackage/__init__.py @@ -2,4 +2,5 @@ from .Script import Script from .Map import Map from .MapTile import MapTile -from .PathFinder import PathFinder \ No newline at end of file +from .PathFinder import PathFinder +from .TradeScript import TradeScript \ No newline at end of file diff --git a/src/OperatingSystemPackage/Widget/GlobalGameWidgetContainer.py b/src/OperatingSystemPackage/Widget/GlobalGameWidgetContainer.py index 13a092e..cc24d56 100644 --- a/src/OperatingSystemPackage/Widget/GlobalGameWidgetContainer.py +++ b/src/OperatingSystemPackage/Widget/GlobalGameWidgetContainer.py @@ -1,4 +1,5 @@ import cv2 +import numpy as np from src.SharedPackage import ScreenRegion, Constants, Coordinate from src.VendorPackage import PyAutoGui, Cv2File @@ -48,6 +49,9 @@ def __init__(self, monitor: Monitor, pyautogui: PyAutoGui, initial_floor_lvl: in Logger.info('Locating Ring Widget...') self.__ring_region = self.__locate_ring_widget() + Logger.info('Locating Nearest Depot...') + self.__nearest_depot_region = self.__locate_depot() + def battle_list_widget(self) -> ScreenRegion: return self.__battle_list_widget_region @@ -75,6 +79,9 @@ def combat_stance_widget(self) -> ScreenRegion: def ring_widget(self) -> ScreenRegion: return self.__ring_region + def nearest_depot(self) -> ScreenRegion: + return self.__nearest_depot_region + def __create_looting_area_coordinates(self,) -> list[Coordinate]: [width, _] = self.__monitor_dimensions @@ -214,3 +221,38 @@ def __locate_ring_widget(self) -> ScreenRegion: end_x = x + width return ScreenRegion(start_x, end_x, start_y, end_y) + def __locate_depot(self) -> ScreenRegion: + frame = cv2.cvtColor(self.__initial_setup_screenshot, cv2.COLOR_BGR2GRAY) + + depot_anchor = Cv2File.load_image(f'src/Wiki/Ui/Market/depot.png') + + match = cv2.matchTemplate(depot_anchor, frame, cv2.TM_CCOEFF_NORMED) + + multiple_loc = np.where(match >= 0.7) + + depot_locations = [] + + for pt in zip(*multiple_loc[::-1]): + depot_locations.append(pt) + + screen_player_position = Coordinate.from_screen_region(self.__game_window) + + distances = [] + for depot_loc in depot_locations: + distance = np.linalg.norm(np.array((screen_player_position.x, screen_player_position.y)) - np.array(depot_loc)) + distances.append(distance) + + # Find the index of the nearest depot + nearest_depot_index = np.argmin(distances) + + # Get the coordinates of the nearest depot + (x, y) = depot_locations[nearest_depot_index] + + height, width = depot_anchor.shape + + return ScreenRegion( + start_x=x, + end_x=x + width, + start_y=y, + end_y=y + height + ) diff --git a/src/SharedPackage/Constants.py b/src/SharedPackage/Constants.py index 4a3fbf3..030e51d 100644 --- a/src/SharedPackage/Constants.py +++ b/src/SharedPackage/Constants.py @@ -21,6 +21,7 @@ class Constants: # APP MODE TRAIN_MODE = "TRAIN_MODE" + TRADE_MODE = "TRADE_MODE" DEBUG_MODE = "DEBUG_MODE" DEV_MODE = "DEV_MODE" PRODUCTION_MODE = "PRODUCTION_MODE" @@ -47,3 +48,7 @@ class Constants: # SPELLS LIGHT_HEALING = "exura infir ico" + + # TRADE + SELL_OFFER = "SELL_OFFER" + BUY_OFFER = "BUY_OFFER" diff --git a/src/SharedPackage/Exception/ManualIterationInterrupt.py b/src/SharedPackage/Exception/ManualIterationInterrupt.py new file mode 100644 index 0000000..45fcc4b --- /dev/null +++ b/src/SharedPackage/Exception/ManualIterationInterrupt.py @@ -0,0 +1,3 @@ +class ManualIterationInterrupt(Exception): + def __init__(self): + super().__init__(f'ManualIterationInterrupt') \ No newline at end of file diff --git a/src/SharedPackage/GameContext.py b/src/SharedPackage/GameContext.py index 55a2175..c55b88a 100644 --- a/src/SharedPackage/GameContext.py +++ b/src/SharedPackage/GameContext.py @@ -1,5 +1,6 @@ from .Creature import Creature from .Waypoint import Waypoint +from .MarketItem import MarketItem from datetime import datetime from copy import deepcopy from src.UtilPackage import Time, LinkedList @@ -37,6 +38,13 @@ def __init__(self): self.__has_to_wear_ring = False self.__is_ring_equipped = False + self.__trade_items = LinkedList[str] + self.__is_market_open = False + self.__is_item_searched = False + self.__is_item_selected = False + self.__is_scrapping_item_info = False + self.__scrapped_item = None + def set_health(self, health: int) -> None: self.__health = health @@ -140,3 +148,39 @@ def get_is_ring_equipped(self) -> bool: def set_is_ring_equipped(self, is_equipped: bool) -> None: self.__is_ring_equipped = is_equipped + + def get_is_market_open(self) -> bool: + return self.__is_market_open + + def set_is_market_open(self, is_market_open: bool) -> None: + self.__is_market_open = is_market_open + + def get_is_item_searched(self) -> bool: + return self.__is_item_searched + + def set_is_item_searched(self, is_item_searched: bool) -> None: + self.__is_item_searched = is_item_searched + + def get_is_item_selected(self) -> bool: + return self.__is_item_selected + + def set_is_item_selected(self, is_item_selected: bool) -> None: + self.__is_item_selected = is_item_selected + + def get_is_scrapping_item_info(self) -> bool: + return self.__is_scrapping_item_info + + def set_is_scrapping_item_info(self, is_scrapping_item_info: bool) -> None: + self.__is_scrapping_item_info = is_scrapping_item_info + + def get_scrapped_item(self) -> MarketItem: + return self.__scrapped_item + + def set_scrapped_item(self, scrapped_item: MarketItem | None) -> None: + self.__scrapped_item = scrapped_item + + def get_trade_items(self) -> LinkedList[str]: + return self.__trade_items + + def set_trade_items(self, items: LinkedList[str]) -> None: + self.__trade_items = items diff --git a/src/SharedPackage/MarketItem.py b/src/SharedPackage/MarketItem.py new file mode 100644 index 0000000..3080389 --- /dev/null +++ b/src/SharedPackage/MarketItem.py @@ -0,0 +1,7 @@ +class MarketItem: + def __init__(self, name: str): + self.name = name + self.offers = list() + + def __str__(self): + return f'MarketItem(name={self.name})' \ No newline at end of file diff --git a/src/SharedPackage/Offer.py b/src/SharedPackage/Offer.py new file mode 100644 index 0000000..3136d67 --- /dev/null +++ b/src/SharedPackage/Offer.py @@ -0,0 +1,9 @@ +class Offer: + def __init__(self, offer_type: str): + self.offer_type = offer_type + self.unit_price = 0 + self.amount = 0 + self.end_date = "" + + def __str__(self): + return f'Offer(offer_type={self.offer_type}, unit_price={self.unit_price}, amount={self.amount}, end_date={self.end_date})' \ No newline at end of file diff --git a/src/SharedPackage/__init__.py b/src/SharedPackage/__init__.py index 67a68fd..6b5684e 100644 --- a/src/SharedPackage/__init__.py +++ b/src/SharedPackage/__init__.py @@ -5,3 +5,6 @@ from .Constants import Constants from .Waypoint import Waypoint from .MoveCommand import MoveCommand +from .MarketItem import MarketItem +from .Offer import Offer +from .Exception.ManualIterationInterrupt import ManualIterationInterrupt diff --git a/src/TaskPackage/GameActions/CancelItemSearch.py b/src/TaskPackage/GameActions/CancelItemSearch.py new file mode 100644 index 0000000..6e29e12 --- /dev/null +++ b/src/TaskPackage/GameActions/CancelItemSearch.py @@ -0,0 +1,73 @@ +from time import sleep + +import numpy as np +import cv2 + +from src.LoggerPackage import Logger +from src.OperatingSystemPackage import GlobalGameWidgetContainer +from src.SharedPackage import GameContext, Coordinate, ScreenRegion, MarketItem +from src.TaskPackage.Task import Task +from src.GamePackage import Player +from src.VendorPackage import Cv2File + +class CancelItemSearch(Task): + def __str__(self) -> str: + return f'CancelItemSearch' + + def __init__(self, widget: GlobalGameWidgetContainer, player: Player): + super().__init__() + self.__widget = widget + self.__player = player + self.__succeed = False + self.__completed = False + + def execute(self, context: GameContext, frame: np.ndarray) -> GameContext: + Logger.debug("Executing CancelItemSearch") + Logger.debug("Received context") + Logger.debug(context, inspect_class=True) + + + grey_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + close_button_region = self.__get_screen_region(grey_frame, 'market_cancel_search') + + close_button_coordinate = Coordinate.from_screen_region(close_button_region) + + self.__player.left_click(Coordinate(close_button_coordinate.x, close_button_coordinate.y)) + + sleep(0.5) + + context.set_is_item_searched(False) + context.set_is_item_selected(False) + context.set_scrapped_item(None) + + item_list = context.get_trade_items() + + if not item_list.peak_next(): + raise KeyboardInterrupt + + item_list.next() + + Logger.debug("Updated context") + Logger.debug(context, inspect_class=True) + + self.success() + return context + + def __get_screen_region(self, grey_frame: np.ndarray, anchor_name: str) -> ScreenRegion: + anchor = Cv2File.load_image(f'src/Wiki/Ui/Market/{anchor_name}.png') + + match = cv2.matchTemplate(grey_frame, anchor, cv2.TM_CCOEFF_NORMED) + + [_, _, _, max_coordinates] = cv2.minMaxLoc(match) + + (x, y) = max_coordinates + + height, width = anchor.shape + + return ScreenRegion( + start_x=x, + end_x=x + width, + start_y=y, + end_y=y + height + ) \ No newline at end of file diff --git a/src/TaskPackage/GameActions/NotifyItemInfo.py b/src/TaskPackage/GameActions/NotifyItemInfo.py new file mode 100644 index 0000000..b5ff4cf --- /dev/null +++ b/src/TaskPackage/GameActions/NotifyItemInfo.py @@ -0,0 +1,40 @@ +import numpy as np +import urllib.request +import urllib.parse + +from src.LoggerPackage import Logger +from src.OperatingSystemPackage import GlobalGameWidgetContainer +from src.SharedPackage import GameContext +from src.TaskPackage.Task import Task + +class NotifyItemInfo(Task): + def __str__(self) -> str: + return f'NotifyItemInfo' + + def __init__(self, widget: GlobalGameWidgetContainer,): + super().__init__() + self.__widget = widget + self.__succeed = False + self.__completed = False + + def execute(self, context: GameContext, frame: np.ndarray) -> GameContext: + Logger.debug("Executing NotifyItemInfo") + Logger.debug("Received context") + Logger.debug(context, inspect_class=True) + + url = 'https://api.example.com/data' + data = {'key': 'value', 'another_key': 'another_value'} + + data = urllib.parse.urlencode(data).encode('utf-8') + req = urllib.request.Request(url, data=data) + + # with urllib.request.urlopen(req) as response: + # body = response.read() + # print(body.decode('utf-8')) + + + Logger.debug("Updated context") + Logger.debug(context, inspect_class=True) + + self.success() + return context diff --git a/src/TaskPackage/GameActions/OpenDepotTask.py b/src/TaskPackage/GameActions/OpenDepotTask.py new file mode 100644 index 0000000..43c2dbd --- /dev/null +++ b/src/TaskPackage/GameActions/OpenDepotTask.py @@ -0,0 +1,39 @@ +import numpy as np + +from src.LoggerPackage import Logger +from src.OperatingSystemPackage import GlobalGameWidgetContainer +from src.SharedPackage import GameContext, Coordinate, ScreenRegion +from src.TaskPackage.Task import Task +from src.GamePackage import Player + + +class OpenDepotTask(Task): + def __str__(self) -> str: + return f'OpenDepotTask' + + def __init__(self, widget: GlobalGameWidgetContainer, player: Player): + super().__init__() + self.__widget = widget + self.__player = player + self.__succeed = False + self.__completed = False + + def execute(self, context: GameContext, frame: np.ndarray) -> GameContext: + try: + Logger.debug("Executing OpenDepotTask") + Logger.debug("Received context") + Logger.debug(context, inspect_class=True) + + nearest_depot_position = self.__widget.nearest_depot() + + self.__player.open(Coordinate.from_screen_region(nearest_depot_position)) + + Logger.debug("Updated context") + Logger.debug(context, inspect_class=True) + + self.success() + return context + except ValueError: + self.fail() + + return context diff --git a/src/TaskPackage/GameActions/OpenMarketTask.py b/src/TaskPackage/GameActions/OpenMarketTask.py new file mode 100644 index 0000000..036bf6f --- /dev/null +++ b/src/TaskPackage/GameActions/OpenMarketTask.py @@ -0,0 +1,59 @@ +import numpy as np +import cv2 + +from src.LoggerPackage import Logger +from src.OperatingSystemPackage import GlobalGameWidgetContainer +from src.SharedPackage import GameContext, Coordinate, ScreenRegion, ManualIterationInterrupt +from src.TaskPackage.Task import Task +from src.GamePackage import Player +from src.VendorPackage import Cv2File + + +class OpenMarketTask(Task): + def __str__(self) -> str: + return f'OpenMarketTask' + + def __init__(self, widget: GlobalGameWidgetContainer, player: Player): + super().__init__() + self.__widget = widget + self.__player = player + self.__succeed = False + self.__completed = False + + def execute(self, context: GameContext, frame: np.ndarray) -> GameContext: + Logger.debug("Executing OpenMarketTask") + Logger.debug("Received context") + Logger.debug(context, inspect_class=True) + + if context.get_is_market_open(): + self.success() + + return context + + market_anchor = Cv2File.load_image(f'src/Wiki/Ui/Market/market_icon.png') + + grey_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + match = cv2.matchTemplate(grey_frame, market_anchor, cv2.TM_CCOEFF_NORMED) + + [_, _, _, max_coordinates] = cv2.minMaxLoc(match) + + (x, y) = max_coordinates + + height, width = market_anchor.shape + + market_region = ScreenRegion( + start_x=x, + end_x=x + width, + start_y=y, + end_y=y + height + ) + + self.__player.open(Coordinate.from_screen_region(market_region)) + + context.set_is_market_open(True) + + Logger.debug("Updated context") + Logger.debug(context, inspect_class=True) + + raise ManualIterationInterrupt diff --git a/src/TaskPackage/GameActions/SearchItemInMarket.py b/src/TaskPackage/GameActions/SearchItemInMarket.py new file mode 100644 index 0000000..a35771b --- /dev/null +++ b/src/TaskPackage/GameActions/SearchItemInMarket.py @@ -0,0 +1,73 @@ +from time import sleep + +import numpy as np +import cv2 + +from src.LoggerPackage import Logger +from src.OperatingSystemPackage import GlobalGameWidgetContainer +from src.SharedPackage import GameContext, Coordinate, ScreenRegion, MarketItem, ManualIterationInterrupt +from src.TaskPackage.Task import Task +from src.GamePackage import Player +from src.VendorPackage import Cv2File + +class SearchItemInMarket(Task): + def __str__(self) -> str: + return f'SearchItemInMarket' + + def __init__(self, widget: GlobalGameWidgetContainer, player: Player): + super().__init__() + self.__widget = widget + self.__player = player + self.__succeed = False + self.__completed = False + + def execute(self, context: GameContext, frame: np.ndarray) -> GameContext: + Logger.debug("Executing SearchItemInMarket") + Logger.debug("Received context") + Logger.debug(context, inspect_class=True) + + if context.get_is_item_searched(): + self.success() + + return context + + item = context.get_trade_items().current.data + grey_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + close_button_region = self.__get_screen_region(grey_frame, 'market_cancel_search') + + close_button_coordinate = Coordinate.from_screen_region(close_button_region) + + self.__player.left_click(Coordinate(close_button_coordinate.x - 20, close_button_coordinate.y)) + + sleep(0.2) + + self.__player.write(item) + + context.set_is_item_searched(True) + context.set_scrapped_item(MarketItem(item)) + + sleep(0.2) + + Logger.debug("Updated context") + Logger.debug(context, inspect_class=True) + + raise ManualIterationInterrupt + + def __get_screen_region(self, grey_frame: np.ndarray, anchor_name: str) -> ScreenRegion: + anchor = Cv2File.load_image(f'src/Wiki/Ui/Market/{anchor_name}.png') + + match = cv2.matchTemplate(grey_frame, anchor, cv2.TM_CCOEFF_NORMED) + + [_, _, _, max_coordinates] = cv2.minMaxLoc(match) + + (x, y) = max_coordinates + + height, width = anchor.shape + + return ScreenRegion( + start_x=x, + end_x=x + width, + start_y=y, + end_y=y + height + ) \ No newline at end of file diff --git a/src/TaskPackage/GameActions/SelectItemInMarket.py b/src/TaskPackage/GameActions/SelectItemInMarket.py new file mode 100644 index 0000000..b53dcf6 --- /dev/null +++ b/src/TaskPackage/GameActions/SelectItemInMarket.py @@ -0,0 +1,72 @@ +from time import sleep + +import numpy as np +import cv2 + +from src.LoggerPackage import Logger +from src.OperatingSystemPackage import GlobalGameWidgetContainer +from src.SharedPackage import GameContext, Coordinate, ScreenRegion, ManualIterationInterrupt +from src.TaskPackage.Task import Task +from src.GamePackage import Player +from src.VendorPackage import Cv2File + +class SelectItemInMarket(Task): + def __str__(self) -> str: + return f'SelectItemInMarket' + + def __init__(self, widget: GlobalGameWidgetContainer, player: Player): + super().__init__() + self.__widget = widget + self.__player = player + self.__succeed = False + self.__completed = False + + def execute(self, context: GameContext, frame: np.ndarray) -> GameContext: + Logger.debug("Executing SelectItemInMarket") + Logger.debug("Received context") + Logger.debug(context, inspect_class=True) + + if context.get_is_item_selected(): + self.success() + + return context + + grey_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + item_list_anchor_screen_region = self.__get_screen_region(grey_frame, 'item_list') + + item_list_screen_region = ScreenRegion( + start_x= item_list_anchor_screen_region.start_x, + end_x= item_list_anchor_screen_region.end_x, + start_y= item_list_anchor_screen_region.start_y, + end_y=item_list_anchor_screen_region.end_y + 40 + ) + + self.__player.left_click(Coordinate.from_screen_region(item_list_screen_region)) + + context.set_is_item_selected(True) + + sleep(1) + + Logger.debug("Updated context") + Logger.debug(context, inspect_class=True) + + raise ManualIterationInterrupt + + def __get_screen_region(self, grey_frame: np.ndarray, anchor_name: str) -> ScreenRegion: + anchor = Cv2File.load_image(f'src/Wiki/Ui/Market/{anchor_name}.png') + + match = cv2.matchTemplate(grey_frame, anchor, cv2.TM_CCOEFF_NORMED) + + [_, _, _, max_coordinates] = cv2.minMaxLoc(match) + + (x, y) = max_coordinates + + height, width = anchor.shape + + return ScreenRegion( + start_x=x, + end_x=x + width, + start_y=y, + end_y=y + height + ) \ No newline at end of file diff --git a/src/TaskPackage/GameContext/Market/ExtractSelectedItemInfo.py b/src/TaskPackage/GameContext/Market/ExtractSelectedItemInfo.py new file mode 100644 index 0000000..3fa2ead --- /dev/null +++ b/src/TaskPackage/GameContext/Market/ExtractSelectedItemInfo.py @@ -0,0 +1,139 @@ +import numpy as np +import cv2 + +from src.LoggerPackage import Logger +from src.OperatingSystemPackage import GlobalGameWidgetContainer +from src.SharedPackage import GameContext, ScreenRegion, Offer, Constants +from src.TaskPackage.Task import Task +from src.UtilPackage import GenericMapCollection +from src.VendorPackage import Cv2File, PyAutoGui + + +class ExtractSelectedItemInfo(Task): + def __str__(self) -> str: + return f'ExtractSelectedItemInfo' + + def __init__(self, widget: GlobalGameWidgetContainer, pyautogui: PyAutoGui): + super().__init__() + self.__widget = widget + self.__pyautogui = pyautogui + self.__succeed = False + self.__completed = False + + def execute(self, context: GameContext, frame: np.ndarray) -> GameContext: + try: + Logger.debug("Executing ExtractSelectedItemInfo") + Logger.debug("Received context") + Logger.debug(context, inspect_class=True) + + grey_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + item = context.get_scrapped_item() + extraction_result = GenericMapCollection[Offer]() + + extraction_result.set(Constants.SELL_OFFER, Offer(Constants.SELL_OFFER)) + extraction_result.set(Constants.BUY_OFFER, Offer(Constants.BUY_OFFER)) + + amount_screen_regions = self.__get_screen_regions(grey_frame, 'amount_column_anchor') + + self.__extract_row_offer(frame, amount_screen_regions, extraction_result, "amount") + + price_screen_regions = self.__get_screen_regions(grey_frame, 'piece_price_column_anchor') + + self.__extract_row_offer(frame, price_screen_regions, extraction_result, "unit_price") + + end_at_screen_region = self.__get_screen_regions(grey_frame, 'ends_at_column_anchor') + + self.__extract_row_offer(frame, end_at_screen_region, extraction_result, "end_date") + + item.offers = [extraction_result.get(Constants.SELL_OFFER), extraction_result.get(Constants.BUY_OFFER)] + + self.success() + return context + except ValueError: + self.fail() + return context + + def __get_screen_regions(self, grey_frame: np.ndarray, anchor_name: str) -> list[ScreenRegion]: + anchor = Cv2File.load_image(f'src/Wiki/Ui/Market/{anchor_name}.png') + + height, width = anchor.shape + + match = cv2.matchTemplate(grey_frame, anchor, cv2.TM_CCOEFF_NORMED) + + multiple_loc = np.where(match >= 0.9) + + screen_regions = [] + + for x, y in zip(*multiple_loc[::-1]): + screen_region = ScreenRegion( + start_x=x, + end_x=x + width, + start_y=y, + end_y=y + height + ) + + screen_regions.append(screen_region) + + return screen_regions + + def __extract_row_offer( + self, + frame: np.ndarray, + screen_regions: list[ScreenRegion], + result_set: GenericMapCollection[Offer], + column: str, + ) -> None: + for region, next_region in zip(screen_regions, screen_regions[1:]): + if region.start_y < next_region.start_y: + # The bottom of the column header + start_y = region.end_y + + # The height of the row + height = region.end_y - region.start_y + end_y = region.end_y + height + + roi = frame[start_y:end_y, region.start_x:region.end_x] + + number_image = self.__preprocess_frame(roi) + + offer = result_set.get(Constants.SELL_OFFER) + + setattr(offer, column, self.__pyautogui.number(number_image)) + + result_set.set(Constants.SELL_OFFER, offer) + + start_y = next_region.end_y + + height = next_region.end_y - next_region.start_y + end_y = next_region.end_y + height + + roi = frame[start_y:end_y, next_region.start_x:next_region.end_x] + + number_image = self.__preprocess_frame(roi) + + offer = result_set.get(Constants.BUY_OFFER) + + setattr(offer, column, self.__pyautogui.number(number_image)) + + result_set.set(Constants.BUY_OFFER, offer) + + def __has_red_pixels(self, frame: np.ndarray) -> bool: + hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) + + lower_red = np.array([0, 100, 50]) + upper_red = np.array([10, 255, 255]) + + mask = cv2.inRange(hsv, lower_red, upper_red) + + return cv2.countNonZero(mask) > 0 + + def __preprocess_frame(self, frame: np.ndarray) -> np.ndarray: + grey_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + if self.__has_red_pixels(frame): + _, thresh = cv2.threshold(grey_frame, 0, 255, cv2.THRESH_OTSU | cv2.THRESH_BINARY_INV) + + return thresh + + return grey_frame diff --git a/src/TaskPackage/__init__.py b/src/TaskPackage/__init__.py index 2395f32..cbdc8f6 100644 --- a/src/TaskPackage/__init__.py +++ b/src/TaskPackage/__init__.py @@ -8,6 +8,8 @@ from .GameContext.ExtractGameContextDataTask import ExtractManaDataTask from .GameContext.ExtractGameContextDataTask import ExtractHealthDataTask from .GameContext.Ring.ExtractRingStatusTask import ExtractRingStatusTask +from .GameContext.Market.ExtractSelectedItemInfo import ExtractSelectedItemInfo + from .GameActions.AttackTask import AttackTask from .GameActions.HealingTask import HealingTask from .GameActions.UseManaSurplusTask import UseManaSurplusTask @@ -18,3 +20,9 @@ from .GameActions.WalkTask import WalkTask from .GameActions.ResolveWaypointActionTask import ResolveWaypointActionTask from .GameActions.EquipRingTask import EquipRingTask +from .GameActions.OpenDepotTask import OpenDepotTask +from .GameActions.OpenMarketTask import OpenMarketTask +from .GameActions.SearchItemInMarket import SearchItemInMarket +from .GameActions.SelectItemInMarket import SelectItemInMarket +from .GameActions.NotifyItemInfo import NotifyItemInfo +from .GameActions.CancelItemSearch import CancelItemSearch diff --git a/src/Trader/AutoTrader.py b/src/Trader/AutoTrader.py new file mode 100644 index 0000000..e860e59 --- /dev/null +++ b/src/Trader/AutoTrader.py @@ -0,0 +1,61 @@ +from src.GamePackage import Player +from src.LoggerPackage import Logger +from src.OperatingSystemPackage import GlobalGameWidgetContainer, Monitor +from src.SharedPackage import GameContext, ManualIterationInterrupt +from src.TaskPackage import TaskResolver, OpenMarketTask, SearchItemInMarket, SelectItemInMarket, ExtractSelectedItemInfo, NotifyItemInfo, CancelItemSearch +from src.VendorPackage import PyAutoGui + + +class AutoTrader: + def __init__( + self, + monitor: Monitor, + task_resolver: TaskResolver, + widget: GlobalGameWidgetContainer, + pyautogui: PyAutoGui, + ): + self.__monitor = monitor + self.__task_resolver = task_resolver + self.__widget = widget + self.__pyautogui = pyautogui + + def start(self, game_context: GameContext, player: Player) -> None: + Logger.info("Starting AutoTrader") + + while True: + try: + frame = self.__monitor.screenshot() + + # Logger.debug('Queuing OpenDepotTask') + # open_depot = OpenDepotTask(self.__widget, player) + # self.__task_resolver.queue(open_depot) + + Logger.debug('Queuing OpenMarketTask') + open_market = OpenMarketTask(self.__widget, player) + self.__task_resolver.queue(open_market) + self.__task_resolver.resolve(game_context, frame) + + Logger.debug('Queuing SearchItemInMarketTask') + search_item = SearchItemInMarket(self.__widget, player) + self.__task_resolver.queue(search_item) + + Logger.debug('Queuing SelectItemInMarket') + select_item = SelectItemInMarket(self.__widget, player) + self.__task_resolver.queue(select_item) + + Logger.debug('Queuing ExtractSelectedItemInfo') + extract_selected_item_info = ExtractSelectedItemInfo(self.__widget, self.__pyautogui) + self.__task_resolver.queue(extract_selected_item_info) + + Logger.debug('Queuing NotifyItemInfo') + notify_item_info = NotifyItemInfo(self.__widget) + self.__task_resolver.queue(notify_item_info) + + Logger.debug('Queuing CancelItemSearch') + cancel_item_search = CancelItemSearch(self.__widget, player) + self.__task_resolver.queue(cancel_item_search) + + self.__task_resolver.resolve(game_context, frame) + + except ManualIterationInterrupt: + continue diff --git a/src/Trader/__init__.py b/src/Trader/__init__.py new file mode 100644 index 0000000..3151bae --- /dev/null +++ b/src/Trader/__init__.py @@ -0,0 +1 @@ +from .AutoTrader import AutoTrader \ No newline at end of file diff --git a/src/Train/AutoTrainer.py b/src/Train/AutoTrainer.py index d833bcf..165d045 100644 --- a/src/Train/AutoTrainer.py +++ b/src/Train/AutoTrainer.py @@ -1,9 +1,8 @@ -from src.GamePackage import Player, Script +from src.GamePackage import Player from src.LoggerPackage import Logger from src.OperatingSystemPackage import GlobalGameWidgetContainer, Monitor from src.SharedPackage import GameContext -from src.TaskPackage import TaskResolver, ExtractHealthDataTask, ExtractManaDataTask, HealingTask, UseManaSurplusTask, \ - EatTask +from src.TaskPackage import TaskResolver, UseManaSurplusTask, EatTask from src.VendorPackage import TesseractOcr diff --git a/src/UtilPackage/GenericMapCollection.py b/src/UtilPackage/GenericMapCollection.py new file mode 100644 index 0000000..64db2f3 --- /dev/null +++ b/src/UtilPackage/GenericMapCollection.py @@ -0,0 +1,23 @@ +from typing import Dict, Generic, TypeVar + +T = TypeVar("T") + +class GenericMapCollection(Generic[T]): + def __init__(self, arg: list[T] | None = None): + if arg is None: + self.dic: Dict[str | int, T] = dict() + return + + self.dic: Dict[str | int, T] = dict(arg) + + def has(self, key: str | int) -> bool: + return key in self.dic + + def get(self, key: str | int) -> T | None: + return self.dic.get(key) + + def set(self, key: str | int, value: T) -> None: + self.dic[key] = value + + def __str__(self) -> str: + return str(self.dic) \ No newline at end of file diff --git a/src/UtilPackage/__init__.py b/src/UtilPackage/__init__.py index 57aea4a..3f26a64 100644 --- a/src/UtilPackage/__init__.py +++ b/src/UtilPackage/__init__.py @@ -4,3 +4,4 @@ from .String import String from .Number import Number from .MapCollection import MapCollection +from .GenericMapCollection import GenericMapCollection diff --git a/src/VendorPackage/PyAutoGui/PyAutoGui.py b/src/VendorPackage/PyAutoGui/PyAutoGui.py index 23bfcfe..024cdfd 100644 --- a/src/VendorPackage/PyAutoGui/PyAutoGui.py +++ b/src/VendorPackage/PyAutoGui/PyAutoGui.py @@ -134,6 +134,20 @@ def locate_mana_widget(self, frame: np.ndarray, monitor_dimensions: tuple[int, i end_y=frame_end_y ) + def locate_image(self, frame: np.ndarray, needle: np.ndarray) -> ScreenRegion: + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + match = cv2.matchTemplate(needle, frame, cv2.TM_CCOEFF_NORMED) + + (y_locations, x_locations) = np.where(match >= NumberCoincidence.MIN_CONFIDENCE) + + return ScreenRegion( + start_x=0, + end_x=0, + start_y=0, + end_y=0 + ) + def screen_size(self) -> (int, int): return pyautogui.size() diff --git a/src/Wiki/Script/Trade/trade.json b/src/Wiki/Script/Trade/trade.json new file mode 100644 index 0000000..79dee3b --- /dev/null +++ b/src/Wiki/Script/Trade/trade.json @@ -0,0 +1,11 @@ +{ + "item": [ + "honeycomb", + "swampling wood", + "broken shamanic staff", + "protective charm", + "tibia coin", + "elven hoof", + "gloom wolf fur" + ] +} \ No newline at end of file diff --git a/src/Wiki/Ui/Market/amount_column_anchor.png b/src/Wiki/Ui/Market/amount_column_anchor.png new file mode 100644 index 0000000..794d02b Binary files /dev/null and b/src/Wiki/Ui/Market/amount_column_anchor.png differ diff --git a/src/Wiki/Ui/Market/depot.png b/src/Wiki/Ui/Market/depot.png new file mode 100644 index 0000000..0bca8ff Binary files /dev/null and b/src/Wiki/Ui/Market/depot.png differ diff --git a/src/Wiki/Ui/Market/ends_at_column_anchor.png b/src/Wiki/Ui/Market/ends_at_column_anchor.png new file mode 100644 index 0000000..349914a Binary files /dev/null and b/src/Wiki/Ui/Market/ends_at_column_anchor.png differ diff --git a/src/Wiki/Ui/Market/item_list.png b/src/Wiki/Ui/Market/item_list.png new file mode 100644 index 0000000..5cf9c1e Binary files /dev/null and b/src/Wiki/Ui/Market/item_list.png differ diff --git a/src/Wiki/Ui/Market/market_cancel_search.png b/src/Wiki/Ui/Market/market_cancel_search.png new file mode 100644 index 0000000..729f748 Binary files /dev/null and b/src/Wiki/Ui/Market/market_cancel_search.png differ diff --git a/src/Wiki/Ui/Market/market_icon.png b/src/Wiki/Ui/Market/market_icon.png new file mode 100644 index 0000000..1b19a8d Binary files /dev/null and b/src/Wiki/Ui/Market/market_icon.png differ diff --git a/src/Wiki/Ui/Market/piece_price_column_anchor.png b/src/Wiki/Ui/Market/piece_price_column_anchor.png new file mode 100644 index 0000000..ee3851c Binary files /dev/null and b/src/Wiki/Ui/Market/piece_price_column_anchor.png differ