├── .gitignore ├── .pylintrc ├── .vscode └── settings.json ├── CHANGES.txt ├── MANIFEST.in ├── README.md ├── VERSION ├── __init__.py ├── mypy.ini ├── pyd2bot.egg-info ├── PKG-INFO ├── SOURCES.txt ├── dependency_links.txt ├── requires.txt └── top_level.txt ├── pyd2bot ├── BotSettings.py ├── Pyd2Bot.py ├── __init__.py ├── apis │ ├── InventoryAPI.py │ ├── PlayerAPI.py │ └── __init__.py ├── data │ ├── __init__.py │ ├── enums.py │ └── models.py ├── farmPaths │ ├── AbstractFarmPath.py │ ├── CustomRandomFarmPath.py │ ├── CyclicFarmPath.py │ ├── PathManager.py │ ├── RandomAreaFarmPath.py │ ├── RandomSubAreaFarmPath.py │ ├── __init__.py │ └── paths.json ├── logic │ ├── __init__.py │ ├── common │ │ ├── __init__.py │ │ ├── frames │ │ │ ├── BotRPCFrame.py │ │ │ ├── BotWorkflowFrame.py │ │ │ └── __init__.py │ │ └── rpcMessages │ │ │ ├── ComeToCollectMessage.py │ │ │ ├── GetCurrentVertexMessage.py │ │ │ ├── GetStatusMessage.py │ │ │ ├── PlayerConnectedMessage.py │ │ │ ├── RCPResponseMessage.py │ │ │ ├── RPCMessage.py │ │ │ └── __init__.py │ ├── fight │ │ ├── __init__.py │ │ ├── behaviors │ │ │ ├── FightPreparation.py │ │ │ ├── FightStateManager.py │ │ │ ├── WatchFightSequence.py │ │ │ ├── __init__.py │ │ │ └── fight_turn │ │ │ │ ├── CastSpell.py │ │ │ │ ├── FightMove.py │ │ │ │ ├── FightPlayTurn.py │ │ │ │ ├── TurnResult.py │ │ │ │ ├── __init__.py │ │ │ │ ├── fight_algo_utils.py │ │ │ │ ├── fight_turn_errors_handling.py │ │ │ │ └── spell_utils.py │ │ ├── fight_seq_diagram.md │ │ ├── frames │ │ │ ├── FightAIFrame.py │ │ │ ├── MuleFightFrame.py │ │ │ └── __init__.py │ │ └── messages │ │ │ ├── MuleSwitchedToCombatContext.py │ │ │ └── __init__.py │ ├── managers │ │ ├── AccountCharactersFetcher.py │ │ ├── AccountManager.py │ │ ├── PathFactory.py │ │ ├── SadidaCharacterCreator.py │ │ └── __init__.py │ └── roleplay │ │ ├── __init__.py │ │ ├── behaviors │ │ ├── AbstractBehavior.py │ │ ├── BehaviorApi.py │ │ ├── TreePrinter.py │ │ ├── __init__.py │ │ ├── bank │ │ │ ├── OpenBank.py │ │ │ ├── RetrieveFromBank.py │ │ │ ├── RetrieveKamasFromBank.py │ │ │ ├── RetrieveRecipeFromBank.py │ │ │ ├── UnloadInBank.py │ │ │ ├── __init__.py │ │ │ ├── retrieval_utils.py │ │ │ ├── scoring.py │ │ │ └── statistical_analysis │ │ │ │ └── sheet1.py │ │ ├── bidhouse │ │ │ ├── EditBidPrice.py │ │ │ ├── GoToMarket.py │ │ │ ├── MarketItemAnalytics.py │ │ │ ├── MarketPersistence.py │ │ │ ├── MarketPersistenceManager.py │ │ │ ├── MonitorMarket.py │ │ │ ├── OpenMarket.py │ │ │ ├── PlaceBid.py │ │ │ ├── RetrieveSellUpdate.py │ │ │ ├── SellItemsFromBag.py │ │ │ ├── UpdateMarketBids.py │ │ │ ├── __init__.py │ │ │ └── doc.md │ │ ├── chat │ │ │ └── GetAllMapFightsDetails.py │ │ ├── craft │ │ │ ├── CraftItem.py │ │ │ └── __init__.py │ │ ├── exchange │ │ │ ├── BotExchange.py │ │ │ ├── CollectItems.py │ │ │ ├── GiveItems.py │ │ │ └── __init__.py │ │ ├── farm │ │ │ ├── AbstractFarmBehavior.py │ │ │ ├── CollectAllMapResources.py │ │ │ ├── CollectableResource.py │ │ │ ├── MultiplePathsResourceFarm.py │ │ │ ├── ResourceFarm.py │ │ │ ├── ResourcesTracker.py │ │ │ └── __init__.py │ │ ├── fight │ │ │ ├── AttackMonsters.py │ │ │ ├── GroupLeaderFarmFights.py │ │ │ ├── MuleFighter.py │ │ │ ├── SoloFarmFights.py │ │ │ └── __init__.py │ │ ├── inventory │ │ │ ├── UseItem.py │ │ │ ├── UseItemsByType.py │ │ │ ├── UseTeleportItem.py │ │ │ └── __init__.py │ │ ├── misc │ │ │ ├── Resurrect.py │ │ │ └── __init__.py │ │ ├── mount │ │ │ ├── PutPetsMount.py │ │ │ ├── ToggleRideMount.py │ │ │ └── __init__.py │ │ ├── movement │ │ │ ├── ActionMapChange.py │ │ │ ├── AstarPathFinder.py │ │ │ ├── AutoTrip.py │ │ │ ├── ChangeMap.py │ │ │ ├── GetOutOfAnkarnam.py │ │ │ ├── InteractiveMapChange.py │ │ │ ├── MapMove.py │ │ │ ├── RequestMapData.py │ │ │ ├── ScrollMapChange.py │ │ │ └── __init__.py │ │ ├── npc │ │ │ ├── NpcDialog.py │ │ │ └── __init__.py │ │ ├── party │ │ │ ├── PartyLeader.py │ │ │ ├── WaitForMembersIdle.py │ │ │ ├── WaitForMembersToShow.py │ │ │ └── __init__.py │ │ ├── quest │ │ │ ├── __init__.py │ │ │ └── treasure_hunt │ │ │ │ ├── ClassicTreasureHunt.py │ │ │ │ ├── FindHintNpc.py │ │ │ │ ├── SolveTreasureHuntStep.py │ │ │ │ ├── TakeTreasureHuntQuest.py │ │ │ │ ├── TreasureHuntPoiDatabase.py │ │ │ │ ├── __init__.py │ │ │ │ ├── hints.json │ │ │ │ └── wrongAnswers.json │ │ ├── server │ │ │ └── __init__.py │ │ ├── skill │ │ │ ├── UseSkill.py │ │ │ └── __init__.py │ │ ├── start │ │ │ ├── ChangeServer.py │ │ │ ├── CreateNewCharacter.py │ │ │ ├── DeleteCharacter.py │ │ │ └── __init__.py │ │ ├── teleport │ │ │ ├── SaveZaap.py │ │ │ ├── TeleportUsingHavenBag.py │ │ │ ├── ToggleHavenBag.py │ │ │ ├── UseZaap.py │ │ │ └── __init__.py │ │ └── updates │ │ │ ├── AutoUpgradeStats.py │ │ │ ├── CollectStats.py │ │ │ └── __init__.py │ │ ├── frames │ │ └── __init__.py │ │ └── messages │ │ ├── AutoTripEndedMessage.py │ │ ├── BankInteractionEndedMessage.py │ │ ├── BankUnloadEndedMessage.py │ │ ├── BankUnloadFailedMessage.py │ │ ├── ExchangeConcludedMessage.py │ │ ├── FollowTransitionMessage.py │ │ ├── MoveToVertexMessage.py │ │ ├── PhenixAutoReviveEndedMessage.py │ │ ├── SellerCollectedGuestItemsMessage.py │ │ ├── SellerVacantMessage.py │ │ ├── TakeNapMessage.py │ │ ├── UnloadInsellerEndedMessage.py │ │ └── __init__.py └── misc │ ├── BotEventsManager.py │ ├── Localizer.py │ ├── NapManager.py │ ├── __init__.py │ ├── areaInfos.json │ └── banks.json ├── pyproject.toml ├── requirements.txt ├── scripts ├── __init__.py ├── fetch_account_data.py ├── fetch_all_acounts.py ├── icon.png ├── icon2.png ├── run_group_fight.py ├── run_solofight_bot.py ├── run_treasureHuntBot.py └── system_tray.py ├── setup.py └── todos /.gitignore: -------------------------------------------------------------------------------- 1 | .buildEnv 2 | build 3 | persistence 4 | __pycache__ 5 | apis.json 6 | certs.json 7 | pyd2bot.egg-info 8 | accounts.json -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | disable= 3 | C0114, # missing-module-docstring 4 | C0303, # trailing-whitespace -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.extraPaths": [ 3 | "../pydofus2", 4 | ] 5 | } -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include pyd2bot *.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyd2bot: A Python Bot for Dofus 2 2 | 3 | Pyd2bot utilizes the Pydofus2 as a background client to automate tasks in Dofus. This guide will help you set up the development environment for Pyd2bot. 4 | 5 | ## Join discord community 6 | 7 | 8 | 9 | ## Prerequisites 10 | 11 | > [!IMPORTANT] 12 | > 13 | >- **Python 3.9.11**: Download and install from [python.org](https://www.python.org/downloads/release/python-3911/). 14 | > 15 | >- **Pcap and Wireshark**: Required for sniffer functionality. Download Wireshark from [here](https://www.wireshark.org/download.html). 16 | > 17 | >- **Make**: Required for updating the protocol. You can install it with with chocolatery, ```shell choco install make```. If you dont have chocolatey install it. 18 | 19 | ## Setup Steps for Developers 20 | 21 | ### 1. Setting Up the Environment 22 | 23 | - **Create a New Folder**: 24 | - Create a new folder named `botdev` and navigate into it: 25 | 26 | ```bash 27 | mkdir botdev 28 | cd botdev 29 | ``` 30 | 31 | - **Clone Repositories**: 32 | - Clone the `pydofus2` repository inside `botdev` folder: 33 | 34 | ```bash 35 | git clone https://github.com/hadamrd/pydofus2.git 36 | ``` 37 | 38 | - Clone the `pyd2bot` repository inside `botdev` folder: 39 | 40 | ```bash 41 | git clone https://github.com/hadamrd/pyd2bot.git 42 | ``` 43 | 44 | - **Create a Virtual Environment**: 45 | - Within the `botdev` folder, create a new fresh python virtual environement: 46 | 47 | ```bash 48 | python -m venv .venv 49 | ``` 50 | 51 | ### 2. Env variables configuration 52 | 53 | Add the following environement variables to your system: 54 | 55 | - `DOFUS_HOME`: Path to your Dofus installation directory. 56 | 57 | If you dont wish to modify your environement variables or if you dont have the admin rights to do so, you can modify the file located at `%APPDATA%/pydofus2/settings.json` and add the following: 58 | 59 | ```json 60 | { 61 | "DOFUS_HOME": "path/to/your/dofus/installation" 62 | } 63 | ``` 64 | 65 | > [!IMPORTANT] 66 | > If Dofus is installed and operational through the Ankama Launcher (Zaap), there is no need to set the `DOFUS_HOME` environment variable or modify the user settings file. PyDofus2 will automatically locate the Dofus installation. 67 | 68 | - `LOGS_DIR`: Path to the folder you want pydofus2 to generate its logs to. 69 | 70 | > [!NOTE] 71 | > If not set the logs will be generated in the `%APPDATA%/pydofus2/logs` folder. 72 | 73 | - `PYBOTDEV_HOME`: Path to you `botdev` directory. 74 | 75 | ### 2. Installing Dependencies 76 | 77 | ```bash 78 | source $PYBOTDEV_HOME/.venv/Script/activate 79 | pip install -e $PYBOTDEV_HOME/pydofus2 80 | pip install -e $PYBOTDEV_HOME/pyd2bot 81 | ``` 82 | 83 | ### 4. Run Unpack Maps to dezip the game maps locally 84 | 85 | ```bash 86 | cd $PYBOTDEV_HOME/pydofus2/updater 87 | make unpack-maps 88 | ``` 89 | 90 | ## To run the bot application 91 | 92 | ```bash 93 | cd $PYBOTDEV_HOME/pydofus2/app 94 | pip install -r requirements.txt 95 | python app.py 96 | ``` 97 | 98 | ### To run the Sniffer App (Optional) 99 | 100 | ```bash 101 | cd $PYBOTDEV_HOME/pydofus2/sniffer 102 | pip install -r requirements.txt 103 | python app.py 104 | ``` 105 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.6.0 -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/__init__.py -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.9 3 | ignore_missing_imports = True 4 | ignore_missing_imports_per_module = True -------------------------------------------------------------------------------- /pyd2bot.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: pyd2bot 3 | Version: 1.4.0 4 | Summary: Python bot that uses pydofus2 as a background client. 5 | Home-page: https://github.com/hadamrd/pyd2bot 6 | Author: majdoub khalid 7 | Author-email: majdoub.khalid@gmail.com 8 | Classifier: Programming Language :: Python :: 3.9.11 9 | Classifier: License :: OSI Approved :: MIT License 10 | Classifier: Operating System :: Windows 11 | Requires-Python: <=3.9.13 12 | Requires-Dist: numpy 13 | Requires-Dist: pydantic 14 | Requires-Dist: pydofus2 15 | 16 | # Pyd2bot: A Python Bot for Dofus 2 17 | 18 | Pyd2bot utilizes the Pydofus2 as a background client to automate tasks in Dofus. This guide will help you set up the development environment for Pyd2bot. 19 | 20 | ## Join discord community 21 | 22 | 23 | 24 | ## Prerequisites 25 | 26 | > [!IMPORTANT] 27 | > 28 | >- **Python 3.9.11**: Download and install from [python.org](https://www.python.org/downloads/release/python-3911/). 29 | > 30 | >- **Pcap and Wireshark**: Required for sniffer functionality. Download Wireshark from [here](https://www.wireshark.org/download.html). 31 | > 32 | >- **Make**: Required for updating the protocol. You can install it with with chocolatery, ```shell choco install make```. If you dont have chocolatey install it. 33 | 34 | ## Setup Steps for Developers 35 | 36 | ### 1. Setting Up the Environment 37 | 38 | - **Create a New Folder**: 39 | - Create a new folder named `botdev` and navigate into it: 40 | 41 | ```bash 42 | mkdir botdev 43 | cd botdev 44 | ``` 45 | 46 | - **Clone Repositories**: 47 | - Clone the `pydofus2` repository inside `botdev` folder: 48 | 49 | ```bash 50 | git clone https://github.com/hadamrd/pydofus2.git 51 | ``` 52 | 53 | - Clone the `pyd2bot` repository inside `botdev` folder: 54 | 55 | ```bash 56 | git clone https://github.com/hadamrd/pyd2bot.git 57 | ``` 58 | 59 | - **Create a Virtual Environment**: 60 | - Within the `botdev` folder, create a new fresh python virtual environement: 61 | 62 | ```bash 63 | python -m venv .venv 64 | ``` 65 | 66 | ### 2. Env variables configuration 67 | 68 | Add the following environement variables to your system: 69 | 70 | - `DOFUS_HOME`: Path to your Dofus installation directory. 71 | 72 | If you dont wish to modify your environement variables or if you dont have the admin rights to do so, you can modify the file located at `%APPDATA%/pydofus2/settings.json` and add the following: 73 | 74 | ```json 75 | { 76 | "DOFUS_HOME": "path/to/your/dofus/installation" 77 | } 78 | ``` 79 | 80 | > [!IMPORTANT] 81 | > If Dofus is installed and operational through the Ankama Launcher (Zaap), there is no need to set the `DOFUS_HOME` environment variable or modify the user settings file. PyDofus2 will automatically locate the Dofus installation. 82 | 83 | - `LOGS_DIR`: Path to the folder you want pydofus2 to generate its logs to. 84 | 85 | > [!NOTE] 86 | > If not set the logs will be generated in the `%APPDATA%/pydofus2/logs` folder. 87 | 88 | - `PYBOTDEV_HOME`: Path to you `botdev` directory. 89 | 90 | ### 2. Installing Dependencies 91 | 92 | ```bash 93 | source $PYBOTDEV_HOME/.venv/Script/activate 94 | pip install -e $PYBOTDEV_HOME/pydofus2 95 | pip install -e $PYBOTDEV_HOME/pyd2bot 96 | ``` 97 | 98 | ### 4. Run Unpack Maps to dezip the game maps locally 99 | 100 | ```bash 101 | cd $PYBOTDEV_HOME/pydofus2/updater 102 | make unpack-maps 103 | ``` 104 | 105 | ## To run the bot application 106 | 107 | ```bash 108 | cd $PYBOTDEV_HOME/pydofus2/app 109 | pip install -r requirements.txt 110 | python app.py 111 | ``` 112 | 113 | ### To run the Sniffer App (Optional) 114 | 115 | ```bash 116 | cd $PYBOTDEV_HOME/pydofus2/sniffer 117 | pip install -r requirements.txt 118 | python app.py 119 | ``` 120 | -------------------------------------------------------------------------------- /pyd2bot.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pyd2bot.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pydantic 3 | pydofus2 4 | -------------------------------------------------------------------------------- /pyd2bot.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | pyd2bot 2 | scripts 3 | -------------------------------------------------------------------------------- /pyd2bot/BotSettings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import threading 4 | from typing import TYPE_CHECKING 5 | from pydofus2.com.ankamagames.dofus.network.enums.BreedEnum import BreedEnum 6 | from pydofus2.damageCalculation.tools.StatIds import StatIds 7 | 8 | BASEDIR = os.path.dirname(os.path.abspath(__file__)) 9 | if TYPE_CHECKING: 10 | from pyd2bot.data.models import Session 11 | 12 | class BotSettings: 13 | PERSISTENCE_DIR = os.path.join(os.getenv("APPDATA"), "pyd2bot", "persistence") 14 | KEYS_DIR = os.path.join(os.getenv("APPDATA"), "pyd2bot", "secrets", "vault") 15 | 16 | if not os.path.exists(PERSISTENCE_DIR): 17 | os.makedirs(PERSISTENCE_DIR) 18 | 19 | if not os.path.exists(KEYS_DIR): 20 | os.makedirs(KEYS_DIR) 21 | 22 | defaultBreedConfig = { 23 | BreedEnum.Sadida: { 24 | "primarySpellId": 13516, # Ronce 25 | "secondarySpellId": 13527, 26 | "treasureHuntFightSpellId": 13577, 27 | "primaryStat": StatIds.STRENGTH, 28 | }, 29 | BreedEnum.Sram: { 30 | "primarySpellId": 12902, # Truanderie 31 | "treasureHuntFightSpellId": 12902, 32 | "primaryStat": StatIds.STRENGTH, 33 | }, 34 | BreedEnum.Cra: { 35 | "primarySpellId": 13047, # Fleche optique 36 | "treasureHuntFightSpellId": 13047, 37 | "primaryStat": StatIds.AGILITY, 38 | }, 39 | BreedEnum.Feca: { 40 | "primarySpellId": 12978, # Attaque naturelle 41 | "treasureHuntFightSpellId": 12978, 42 | "primaryStat": StatIds.INTELLIGENCE, 43 | } 44 | } 45 | 46 | SELLER_VACANT = threading.Event() 47 | SELLER_LOCK = threading.Lock() 48 | 49 | # Static constants for random range 50 | MIN_NAP_AFTER_HOURS = 2.0 51 | MAX_NAP_AFTER_HOURS = 4.0 52 | MIN_NAP_DURATION_MINUTES = 10.0 53 | MAX_NAP_DURATION_MINUTES = 50.0 54 | REST_TIME_BETWEEN_HUNTS = 20 55 | 56 | @classmethod 57 | def generate_random_nap_timeout(cls): 58 | """Generate random nap timeout in hours in a predefined range""" 59 | return random.uniform(cls.MIN_NAP_AFTER_HOURS, cls.MAX_NAP_AFTER_HOURS) 60 | 61 | @classmethod 62 | def generate_random_nap_duration(cls): 63 | return random.uniform(cls.MIN_NAP_DURATION_MINUTES, cls.MAX_NAP_DURATION_MINUTES) 64 | 65 | @classmethod 66 | def checkBreed(self, session: "Session"): 67 | if session.character.breedId not in BotSettings.defaultBreedConfig: 68 | supported_breeds = [BreedEnum.get_name(breedId) for breedId in BotSettings.defaultBreedConfig] 69 | raise ValueError( 70 | f"Breed {session.character.breedName} is not supported, supported breeds are {supported_breeds}" 71 | ) 72 | -------------------------------------------------------------------------------- /pyd2bot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/__init__.py -------------------------------------------------------------------------------- /pyd2bot/apis/InventoryAPI.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from pydofus2.com.ankamagames.dofus.internalDatacenter.items.ItemWrapper import ItemWrapper 3 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 4 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.InventoryManager import \ 5 | InventoryManager 6 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 7 | PlayedCharacterManager 8 | from pydofus2.com.ankamagames.dofus.logic.game.roleplay.actions.DeleteObjectAction import \ 9 | DeleteObjectAction 10 | 11 | 12 | class InventoryAPI: 13 | 14 | @classmethod 15 | def getWeightPercent(cls): 16 | pourcentt = round( 17 | (PlayedCharacterManager().inventoryWeight / PlayedCharacterManager().inventoryWeightMax) * 100, 18 | 2, 19 | ) 20 | return pourcentt 21 | 22 | @classmethod 23 | def destroyAllItems(cls): 24 | for iw in InventoryManager().realInventory: 25 | if not iw.isEquipment: 26 | doa = DeleteObjectAction.create(iw.objectUID, iw.quantity) 27 | Kernel().worker.process(doa) 28 | 29 | @classmethod 30 | def getItemFromInventoryByGID(self, object_gid: int) -> Optional[ItemWrapper]: 31 | """Get item wrapper from inventory by GID""" 32 | if object_gid in ItemWrapper._cacheGId: 33 | return ItemWrapper._cacheGId[object_gid] 34 | 35 | inventory = InventoryManager().inventory.getView("real").content 36 | for item in inventory: 37 | if item.objectGID == object_gid: 38 | return item 39 | 40 | return None 41 | 42 | @classmethod 43 | def getItemFromBankByGID(self, object_gid: int) -> Optional[ItemWrapper]: 44 | """Get item wrapper from inventory by GID""" 45 | if object_gid in ItemWrapper._cacheGId: 46 | return ItemWrapper._cacheGId[object_gid] 47 | 48 | inventory = InventoryManager().bankInventory.getView("bank").content 49 | for item in inventory: 50 | if item.objectGID == object_gid: 51 | return item 52 | 53 | return None -------------------------------------------------------------------------------- /pyd2bot/apis/PlayerAPI.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 2 | from pyd2bot.logic.roleplay.behaviors.fight.MuleFighter import MuleFighter 3 | from pydofus2.com.ankamagames.atouin.managers.MapDisplayManager import \ 4 | MapDisplayManager 5 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 6 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 7 | PlayedCharacterManager 8 | from pydofus2.com.ankamagames.jerakine.metaclass.Singleton import Singleton 9 | 10 | 11 | class PlayerAPI(metaclass=Singleton): 12 | def __init__(self): 13 | pass 14 | 15 | def status(self, instanceId) -> str: 16 | for behavior in AbstractBehavior.getSubs(instanceId): 17 | if type(behavior) != MuleFighter and behavior.isRunning(): 18 | return f"Running:{type(behavior).__name__}" 19 | if PlayedCharacterManager.getInstance(instanceId).isInFight: 20 | status = "fighting" 21 | elif MapDisplayManager.getInstance(instanceId).currentDataMap is None: 22 | status = "loadingMap" 23 | elif not Kernel.getInstance(instanceId).roleplayEntitiesFrame: 24 | status = "outOfRolePlay" 25 | elif not Kernel.getInstance(instanceId).roleplayEntitiesFrame.mcidm_processed: 26 | status = "processingMapData" 27 | else: 28 | status = "idle" 29 | return status 30 | -------------------------------------------------------------------------------- /pyd2bot/apis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/apis/__init__.py -------------------------------------------------------------------------------- /pyd2bot/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/data/__init__.py -------------------------------------------------------------------------------- /pyd2bot/data/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ServerNotificationEnum: 5 | FULL_PODS = 756273 6 | DOESNT_HAVE_LVL_FOR_HAVEN_BAG = 589049 7 | CANT_USE_HAVEN_BAG_FROM_CURRMAP = 589088 8 | MOUNT_HAS_NO_ENERGY_LEFT = 5336 9 | INACTIVITY_WARNING = 5123 10 | KAMAS_GAINED = 325840 11 | KAMAS_LOST = 325865 12 | NOT_ENOUGH_KAMAS = 325825 13 | CANT_SELL_ANYMORE_ITEMS = 5150 14 | WAIT_REQUIRED = 4852 15 | CANT_TAKE_ALL_OBJECTS = 5023 16 | ITEM_SOLD = 378533 17 | BANK_OPEN_TAX = 325835 18 | STATUS_DOES_NOT_ALLOW_ACTION = 5268 19 | 20 | class SessionTypeEnum(str, Enum): 21 | SOLO_FIGHT = 'SOLO_FIGHT' 22 | GROUP_FIGHT = 'GROUP_FIGHT' 23 | FARM = 'FARM' 24 | SELL = 'SELL' 25 | TREASURE_HUNT = 'TREASURE_HUNT' 26 | MIXED = 'MIXED' 27 | MULE_FIGHT = 'MULE_FIGHT' 28 | MULTIPLE_PATHS_FARM = 'MULTIPLE_PATHS_FARM' 29 | 30 | class UnloadTypeEnum(str, Enum): 31 | BANK = 'BANK' 32 | STORAGE = 'STORAGE' 33 | SELLER = 'SELLER' 34 | 35 | class PathTypeEnum(str, Enum): 36 | RandomSubAreaFarmPath = 'RandomSubAreaFarmPath' 37 | RandomAreaFarmPath = 'RandomAreaFarmPath' 38 | CyclicFarmPath = 'CyclicFarmPath' 39 | CustomRandomFarmPath = 'CustomRandomFarmPath' -------------------------------------------------------------------------------- /pyd2bot/farmPaths/CustomRandomFarmPath.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | from typing import Iterator 4 | from pyd2bot.farmPaths.AbstractFarmPath import AbstractFarmPath 5 | from pyd2bot.farmPaths.RandomAreaFarmPath import NoTransitionFound 6 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.Edge import \ 7 | Edge 8 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.Vertex import \ 9 | Vertex 10 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.WorldGraph import \ 11 | WorldGraph 12 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 13 | 14 | 15 | class CustomRandomFarmPath(AbstractFarmPath): 16 | def __init__( 17 | self, 18 | name: str, 19 | mapIds: list[int] 20 | ) -> None: 21 | super().__init__() 22 | self.name = name 23 | self._mapIds = mapIds 24 | random_start_map = random.choice(self._mapIds) 25 | self.startVertex = WorldGraph().getVertex(random_start_map, 1) 26 | # Cache for edge count 27 | self._edge_count = None 28 | 29 | @property 30 | def mapIds(self) -> list[int]: 31 | return self._mapIds 32 | 33 | def init(self): 34 | Logger().info(f"CustomRandomFarmPath {self.name} initialized with {len(self.vertices)} vertices") 35 | vertex_count, edge_count = self.calculate_graph_size() 36 | 37 | def __next__(self, forbiddenEdges=None) -> Edge: 38 | outgoingEdges = list(self.outgoingEdges(onlyNonRecentVisited=False)) 39 | if forbiddenEdges is None: 40 | forbiddenEdges = [] 41 | outgoingEdges = [e for e in outgoingEdges if e not in forbiddenEdges] 42 | if not outgoingEdges: 43 | raise NoTransitionFound() 44 | edge = random.choice(outgoingEdges) 45 | return edge 46 | 47 | def currNeighbors(self) -> Iterator[Vertex]: 48 | return self.outgoingEdges(self.currentVertex) 49 | 50 | def outgoingEdges(self, vertex=None, onlyNonRecentVisited=False) -> Iterator[Edge]: 51 | if vertex is None: 52 | vertex = self.currentVertex 53 | outgoingEdges = WorldGraph().getOutgoingEdgesFromVertex(vertex, False, False) 54 | ret = [] 55 | for edge in outgoingEdges: 56 | if edge.dst.mapId in self.mapIds: 57 | if self.hasValidTransition(edge): 58 | if onlyNonRecentVisited: 59 | if edge.dst in self._lastVisited: 60 | if time.perf_counter() - self._lastVisited[edge.dst] > 60 * 60: 61 | ret.append(edge) 62 | else: 63 | ret.append(edge) 64 | else: 65 | ret.append(edge) 66 | return ret 67 | 68 | def getNextEdge(self, forbiddenEdges=None, onlyNonRecent=False) -> Edge: 69 | outgoingEdges = list(self.outgoingEdges(onlyNonRecentVisited=onlyNonRecent)) 70 | if forbiddenEdges is None: 71 | forbiddenEdges = [] 72 | outgoingEdges = [e for e in outgoingEdges if e not in forbiddenEdges] 73 | if not outgoingEdges: 74 | raise NoTransitionFound() 75 | edge = random.choice(outgoingEdges) 76 | return edge 77 | -------------------------------------------------------------------------------- /pyd2bot/farmPaths/CyclicFarmPath.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import Set, List 3 | from pyd2bot.farmPaths.AbstractFarmPath import AbstractFarmPath 4 | from pyd2bot.farmPaths.RandomAreaFarmPath import NoTransitionFound 5 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 6 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.astar.AStar import AStar 7 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.Edge import Edge 8 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.Vertex import Vertex 9 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.WorldGraph import WorldGraph 10 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 11 | from time import time 12 | from random import choice 13 | 14 | class CyclicFarmPath(AbstractFarmPath): 15 | def __init__(self, name: str, mapIds: List[int]) -> None: 16 | super().__init__() 17 | self.name = name 18 | self._mapIds = mapIds 19 | self.startVertex = WorldGraph().getVertex(mapIds[0], 1) 20 | self._next_edges = defaultdict(list) # vertex -> list of possible next edges 21 | self._complete_path: List[Edge] = [] 22 | self._vertex_history = defaultdict(float) # vertex -> last visit timestamp 23 | 24 | @property 25 | def mapIds(self) -> List[int]: 26 | return self._mapIds 27 | 28 | def init(self): 29 | Logger().debug(f"Running cyclic farm path with mapIds : {self.mapIds}") 30 | self._build_complete_path() 31 | # Add this to init() to debug the path 32 | Logger().debug(f"Path sequence: {[edge.src.mapId for edge in self._complete_path]}") 33 | Logger().debug(f"Total unique maps in path: {len(set(edge.src.mapId for edge in self._complete_path))}") 34 | 35 | def _build_complete_path(self): 36 | """Builds the complete cyclic path connecting all maps.""" 37 | self._complete_path = [] 38 | self._next_edges.clear() 39 | self._vertices.clear() 40 | 41 | # Build path segments between consecutive maps 42 | for i in range(len(self._mapIds)): 43 | current_map_id = self._mapIds[i] 44 | next_map_id = self._mapIds[(i + 1) % len(self._mapIds)] 45 | 46 | src_vertex = WorldGraph().getVertex(current_map_id, 1) 47 | dst_vertex = WorldGraph().getVertex(next_map_id, 1) 48 | 49 | if not src_vertex or not dst_vertex: 50 | raise Exception(f"Could not find vertices for maps {current_map_id} -> {next_map_id}") 51 | 52 | path_segment = AStar().search(src_vertex, dst_vertex) 53 | if not path_segment: 54 | raise Exception(f"No path found between maps {current_map_id} -> {next_map_id}") 55 | 56 | self._complete_path.extend(path_segment) 57 | 58 | # Build the next_edges mapping from path segment 59 | for edge in path_segment: 60 | self._next_edges[edge.src].append(edge) 61 | self._vertices.add(edge.src) 62 | self._vertices.add(edge.dst) 63 | 64 | # Add final connection to make it cyclic 65 | if self._complete_path: 66 | last_vertex = self._complete_path[-1].dst 67 | first_vertex = self._complete_path[0].src 68 | if last_vertex != first_vertex: 69 | closing_path = AStar().search(last_vertex, first_vertex) 70 | if closing_path: 71 | self._complete_path.extend(closing_path) 72 | for edge in closing_path: 73 | self._next_edges[edge.src].append(edge) 74 | self._vertices.add(edge.src) 75 | self._vertices.add(edge.dst) 76 | 77 | def __next__(self, forbiddenEdges=None) -> Edge: 78 | """Get next edge in cycle.""" 79 | return self.getNextEdge(forbiddenEdges) 80 | 81 | def getNextEdge(self, forbiddenEdges=None, onlyNonRecent=False) -> Edge: 82 | """Get the next edge to follow in the path.""" 83 | curr_vertex = PlayedCharacterManager().currVertex 84 | 85 | if not self._next_edges[curr_vertex]: 86 | raise NoTransitionFound("Current position not in path and could not find way back!") 87 | 88 | possible_edges = self._next_edges[curr_vertex] 89 | recently_visited = {v for v, t in self._vertex_history.items() if time() - t < 60} 90 | 91 | non_recent_edges = [e for e in possible_edges if e.dst not in recently_visited] 92 | chosen_edge = choice(non_recent_edges) if non_recent_edges else choice(possible_edges) 93 | 94 | self._vertex_history[curr_vertex] = time() 95 | return chosen_edge 96 | 97 | @property 98 | def vertices(self) -> Set[Vertex]: 99 | """Get all vertices in the path.""" 100 | if not self._vertices: 101 | self._build_complete_path() 102 | return self._vertices -------------------------------------------------------------------------------- /pyd2bot/farmPaths/PathManager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from pyd2bot.data.models import Path 5 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 6 | 7 | __dir__ = os.path.dirname(os.path.abspath(__file__)) 8 | default_paths_json_file = os.path.join(__dir__, "paths.json") 9 | 10 | 11 | class PathManager: 12 | _paths = dict[str, Path]() 13 | _data_file = default_paths_json_file 14 | 15 | with open(_data_file, 'r') as fp: 16 | paths_list = json.load(fp) 17 | for path in paths_list: 18 | _paths[path['id']] = Path(**path) 19 | 20 | @classmethod 21 | def get_path(cls, path_name) -> Path: 22 | if path_name not in cls._paths: 23 | raise Exception(f"Path {path_name} not found") 24 | return cls._paths[path_name] -------------------------------------------------------------------------------- /pyd2bot/farmPaths/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/farmPaths/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/common/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/common/frames/BotWorkflowFrame.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.data.models import Session 2 | from pyd2bot.logic.common.rpcMessages.PlayerConnectedMessage import \ 3 | PlayerConnectedMessage 4 | from pyd2bot.logic.roleplay.messages.SellerVacantMessage import \ 5 | SellerVacantMessage 6 | from pyd2bot.misc.BotEventsManager import BotEventsManager 7 | from pyd2bot.data.enums import ServerNotificationEnum 8 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 9 | from pydofus2.com.ankamagames.berilia.managers.KernelEventsManager import \ 10 | KernelEventsManager 11 | from pydofus2.com.ankamagames.dofus.kernel.net.ConnectionsHandler import \ 12 | ConnectionsHandler 13 | from pydofus2.com.ankamagames.dofus.kernel.net.PlayerDisconnectedMessage import \ 14 | PlayerDisconnectedMessage 15 | from pydofus2.com.ankamagames.dofus.network.messages.common.basic.BasicPingMessage import \ 16 | BasicPingMessage 17 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 18 | from pydofus2.com.ankamagames.jerakine.messages.Frame import Frame 19 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 20 | from pydofus2.com.ankamagames.jerakine.types.enums.Priority import Priority 21 | 22 | 23 | class BotWorkflowFrame(Frame): 24 | def __init__(self, session: Session): 25 | self.currentContext = None 26 | self.session = session 27 | super().__init__() 28 | 29 | def pushed(self) -> bool: 30 | KernelEventsManager().on(KernelEvent.ServerTextInfo, self.onServerNotif, originator=self) 31 | return True 32 | 33 | def pulled(self) -> bool: 34 | return True 35 | 36 | @property 37 | def priority(self) -> int: 38 | return Priority.VERY_LOW 39 | 40 | def process(self, msg: Message) -> bool: 41 | 42 | if isinstance(msg, PlayerConnectedMessage): 43 | Logger().info(f"Bot {msg.instanceId} connected") 44 | BotEventsManager().send(BotEventsManager.BOT_CONNECTED, msg.instanceId) 45 | return True 46 | 47 | elif isinstance(msg, PlayerDisconnectedMessage): 48 | Logger().info(f"Bot {msg.instanceId} disconnected") 49 | BotEventsManager().send(BotEventsManager.PLAYER_DISCONNECTED, msg.instanceId, msg.connectionType) 50 | return True 51 | 52 | elif isinstance(msg, SellerVacantMessage): 53 | Logger().info(f"Seller {msg.instanceId} is vacant") 54 | BotEventsManager().send(BotEventsManager.SELLER_AVAILABLE, msg.instanceId) 55 | return True 56 | 57 | def onServerNotif(self, event, msgId, msgType, textId, text, params): 58 | # if textId == ServerNotificationEnum.INACTIVITY_WARNING: 59 | # if not self.session.isSeller: 60 | # KernelEventsManager().send(KernelEvent.ClientRestart, "Bot stayed inactive for too long, must have a bug") 61 | # else: 62 | # pingMsg = BasicPingMessage() 63 | # pingMsg.init(True) 64 | # ConnectionsHandler().send(pingMsg) 65 | if textId == ServerNotificationEnum.ITEM_SOLD: 66 | kamas, gid, gid, qty = list(map(int, params)) 67 | KernelEventsManager().send(KernelEvent.ItemSold, gid, qty, kamas) 68 | -------------------------------------------------------------------------------- /pyd2bot/logic/common/frames/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/common/frames/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/common/rpcMessages/ComeToCollectMessage.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.common.rpcMessages.RPCMessage import RPCMessage 2 | from pyd2bot.misc.Localizer import BankInfos 3 | from pyd2bot.data.models import Character 4 | 5 | 6 | class ComeToCollectMessage(RPCMessage): 7 | def __init__(self, dest, bankInfos: BankInfos, guestInfos: Character) -> None: 8 | super().__init__(dest) 9 | self.bankInfos = bankInfos 10 | self.guestInfos = guestInfos 11 | self.oneway = False 12 | -------------------------------------------------------------------------------- /pyd2bot/logic/common/rpcMessages/GetCurrentVertexMessage.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.common.rpcMessages.RPCMessage import RPCMessage 2 | 3 | 4 | class GetCurrentVertexMessage(RPCMessage): 5 | def __init__(self, dest): 6 | super().__init__(dest) 7 | self.oneway = False 8 | -------------------------------------------------------------------------------- /pyd2bot/logic/common/rpcMessages/GetStatusMessage.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.common.rpcMessages.RPCMessage import RPCMessage 2 | 3 | 4 | class GetStatusMessage(RPCMessage): 5 | def __init__(self, dst: str): 6 | super().__init__(dst) 7 | self.oneway = False 8 | -------------------------------------------------------------------------------- /pyd2bot/logic/common/rpcMessages/PlayerConnectedMessage.py: -------------------------------------------------------------------------------- 1 | class PlayerConnectedMessage: 2 | 3 | def __init__(self, instance) -> None: 4 | self.instanceId = instance 5 | 6 | def __str__(self) -> str: 7 | return f"BotConnect({self.instanceId})" -------------------------------------------------------------------------------- /pyd2bot/logic/common/rpcMessages/RCPResponseMessage.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.common.rpcMessages.RPCMessage import RPCMessage 2 | 3 | 4 | class RPCResponseMessage(RPCMessage): 5 | 6 | def __init__(self, callerMsg: RPCMessage, data=None) -> None: 7 | super().__init__(callerMsg.sender, data) 8 | self.oneway = True 9 | self.reqUid = callerMsg.uid 10 | -------------------------------------------------------------------------------- /pyd2bot/logic/common/rpcMessages/RPCMessage.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import uuid 3 | from time import perf_counter 4 | 5 | 6 | class RPCMessage: 7 | def __init__(self, dst: str, data=None) -> None: 8 | self.oneway = None 9 | self.uid = uuid.uuid1() 10 | self.creationTime = perf_counter() 11 | self.reqUid = None 12 | self.sender = threading.current_thread().name 13 | self.dest = dst 14 | self.data = data 15 | 16 | def __str__(self) -> str: 17 | return f"{type(self).__name__}({self.sender}, {self.dest}, {self.data})" -------------------------------------------------------------------------------- /pyd2bot/logic/common/rpcMessages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/common/rpcMessages/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/fight/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/fight/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/fight/behaviors/WatchFightSequence.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 2 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 3 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 4 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 5 | 6 | 7 | class WatchFightSequence(AbstractBehavior): 8 | 9 | def __init__(self): 10 | super().__init__() 11 | self._active_sequences = 0 12 | 13 | def run(self) -> bool: 14 | self.once(KernelEvent.FightSequenceStart, self._on_fight_sequence_start) 15 | self.once(KernelEvent.FightSequenceEnd, self._on_fight_sequence_end) 16 | 17 | def _on_fight_sequence_start(self, event) -> None: 18 | self._active_sequences += 1 19 | 20 | def _on_fight_sequence_end(self, event) -> None: 21 | self._active_sequences = max(0, self._active_sequences - 1) 22 | if self._active_sequences == 0: 23 | if Kernel().battleFrame.is_sequence_executing(): 24 | Logger().info("Waiting for sequences to end before trigger sequence listeners") 25 | self.once(KernelEvent.SequenceExecFinished, lambda *_: self.callback(0)) 26 | return True 27 | Logger().debug("Sequence finished") 28 | self.callback(0) 29 | -------------------------------------------------------------------------------- /pyd2bot/logic/fight/behaviors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/fight/behaviors/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/fight/behaviors/fight_turn/CastSpell.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | import random 3 | from pyd2bot.logic.fight.behaviors.FightStateManager import FightStateManager 4 | from pyd2bot.logic.fight.behaviors.fight_turn.spell_utils import can_cast_spell_on_cell, check_line_of_sight 5 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 6 | from pydofus2.com.ankamagames.atouin.HaapiEventsManager import HaapiEventsManager 7 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 8 | from pydofus2.com.ankamagames.dofus.internalDatacenter.spells.SpellWrapper import SpellWrapper 9 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.InactivityManager import InactivityManager 10 | from pydofus2.com.ankamagames.dofus.network.messages.game.actions.fight.GameActionFightCastRequestMessage import GameActionFightCastRequestMessage 11 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 12 | 13 | 14 | class CastSpell(AbstractBehavior): 15 | 16 | class errors(Enum): 17 | NO_LOS = auto() 18 | UNEXPECTED_SPELL_CAST = auto() 19 | SPELL_CAST_FAILED = auto() 20 | CANT_CAST_SPELL = auto() 21 | NO_FIGHTER_POS = auto() 22 | 23 | def __init__(self, spellw: SpellWrapper, target_cellId: int): 24 | super().__init__() 25 | self.spellw = spellw 26 | self.target_cellId = target_cellId 27 | self._cast_spell_request_sent = False 28 | self.state_manager = FightStateManager() 29 | 30 | def run(self) -> bool: 31 | """Start travel to marketplace""" 32 | self.once(KernelEvent.SpellCastFailed, self.on_spell_cast_failed) 33 | self.on(KernelEvent.FighterCastedSpell, self.on_spell_casted) 34 | self.castSpell() 35 | 36 | def castSpell(self) -> None: 37 | Logger().info(f"Casting spell {self.spellw.spellId} on cell {self.target_cellId}") 38 | fighterPos = self.state_manager.fighter_pos 39 | if not fighterPos: 40 | self.finish(self.errors.NO_FIGHTER_POS, "Couldn't find fighter position!") 41 | return 42 | 43 | if not self._cast_spell_request_sent: 44 | canCast, reason = can_cast_spell_on_cell(self.spellw, self.target_cellId) 45 | if canCast: 46 | has_los, los_reason = check_line_of_sight(fighterPos.cellId, self.target_cellId) 47 | if not has_los: 48 | Logger().error(f"Can't cast spell {self.spellw.spellId} on cell {self.target_cellId}: {los_reason}") 49 | self.finish(self.errors.NO_LOS, "Cast spell no LOS") 50 | return 51 | self.send_cast_spell_request() 52 | else: 53 | self.finish(self.errors.CANT_CAST_SPELL, f"Cant cast spell for reason : {reason}") 54 | 55 | def _handle_server_info(self, event, msgId, msgType, textId, text, params): 56 | """Handle server info messages""" 57 | if textId == 144451: # Line of sight blocked 58 | Logger().warning("Line of sight blocked") 59 | self.finish(self.errors.NO_LOS, "Cast spell no LOS") 60 | 61 | def send_cast_spell_request(self): 62 | self._cast_spell_request_sent = True 63 | message = GameActionFightCastRequestMessage() 64 | message.init(self.spellw.spellId, self.target_cellId) 65 | self.state_manager.curr_player_connection.send(message) 66 | InactivityManager().activity() 67 | if random.random() < 0.9: 68 | HaapiEventsManager().registerShortcutUse('useSpellLine1') 69 | 70 | def on_spell_casted(self, event, sourceId, destinationCellId, sourceCellId, spellId): 71 | if sourceId == self.state_manager.current_player.id and self.target_cellId == destinationCellId: 72 | event.listener.delete() 73 | if self._cast_spell_request_sent: 74 | Logger().info(f"Spell {self.spellw.spell.name} casted successfully!") 75 | self.finish(0) 76 | else: 77 | self.finish(self.errors.UNEXPECTED_SPELL_CAST, f"A Spell was casted but the player didn't request any!") 78 | 79 | def on_spell_cast_failed(self, event): 80 | if not self.state_manager.current_player : 81 | return self.finish(0) 82 | 83 | if self._cast_spell_request_sent: 84 | self.finish(self.errors.SPELL_CAST_FAILED, "Failed to cast spell!") 85 | -------------------------------------------------------------------------------- /pyd2bot/logic/fight/behaviors/fight_turn/TurnResult.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class TurnResult(Enum): 5 | SUCCESS = 0 6 | DISCONNECTED = 1 7 | NO_TARGETS = 2 8 | CANNOT_CAST = 3 9 | NO_PATH = 4 10 | PLAYER_DEAD = 5 11 | NO_FIGHTER_INFO = 6 12 | CANT_FIND_WAY_TO_HIT = 7 13 | INVALID_STATE = 8 -------------------------------------------------------------------------------- /pyd2bot/logic/fight/behaviors/fight_turn/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/fight/behaviors/fight_turn/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/fight/behaviors/fight_turn/fight_turn_errors_handling.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional, TYPE_CHECKING 2 | from pyd2bot.logic.fight.behaviors.fight_turn.CastSpell import CastSpell 3 | from pyd2bot.logic.fight.behaviors.fight_turn.FightMove import FightMoveBehavior 4 | from pyd2bot.logic.fight.behaviors.fight_turn.TurnResult import TurnResult 5 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 6 | if TYPE_CHECKING: 7 | from pyd2bot.logic.fight.behaviors.fight_turn.FightPlayTurn import FightPlayTurn 8 | 9 | def handle_move_result(behavior: "FightPlayTurn", error_code: int, error_msg: str, infos: Optional[Dict] = None) -> None: 10 | """Handle movement action results""" 11 | # Success 12 | if error_code == 0: 13 | behavior._current_retry_count = 0 14 | behavior.next_action() 15 | return 16 | 17 | # Handle blocked cells first if info available 18 | if infos and infos.get("blocked_cell"): 19 | behavior._forbidden_cells.add(infos['blocked_cell']) 20 | Logger().info(f"Marked cell {infos['blocked_cell']} as blocked") 21 | if infos.get("stopped_at"): 22 | Logger().info(f"Movement stopped at cell {infos['stopped_at']}") 23 | 24 | # Map error codes to retry decisions 25 | movement_retry_map = { 26 | FightMoveBehavior.errors.NO_FIGHTER_POS: (False, True), # (can_retry, end_turn) 27 | FightMoveBehavior.errors.PATH_BLOCKED: (True, False), 28 | FightMoveBehavior.errors.INSUFFICIENT_MP: (False, True), 29 | FightMoveBehavior.errors.MOVEMENT_FAILED: (True, False), 30 | FightMoveBehavior.errors.INVALID_PATH: (False, True), 31 | FightMoveBehavior.errors.MAX_RETRIES_EXCEEDED: (False, True) 32 | } 33 | 34 | can_retry, should_end_turn = movement_retry_map.get(error_code, (False, True)) 35 | 36 | # If we've exceeded max retries, force end turn 37 | if behavior._current_retry_count >= 3: 38 | should_end_turn = True 39 | can_retry = False 40 | 41 | if should_end_turn: 42 | Logger().warning(f"Movement failed with non-recoverable error: {error_msg}") 43 | behavior._end_turn(TurnResult.NO_PATH, error_msg) 44 | return 45 | 46 | # Restart turn planning with updated forbidden cells 47 | if can_retry: 48 | Logger().info(f"Retrying turn after movement error: {error_msg}") 49 | behavior._current_retry_count += 1 50 | behavior._action_queue.clear() 51 | behavior.main() 52 | 53 | else: 54 | Logger().warning(f"Movement failed after retries: {error_msg}") 55 | behavior._end_turn(TurnResult.NO_PATH, error_msg) 56 | 57 | def handle_spell_result(behavior: "FightPlayTurn", error_code: int, error_msg: str, infos: Optional[Dict] = None) -> None: 58 | """Handle spell casting action results""" 59 | if error_code == 0: # Success 60 | behavior.next_action() 61 | return 62 | 63 | # Map error codes to retry decisions 64 | spell_retry_map = { 65 | CastSpell.errors.NO_LOS.value: (True, False), # (can_retry, end_turn) 66 | CastSpell.errors.UNEXPECTED_SPELL_CAST.value: (False, True), 67 | CastSpell.errors.SPELL_CAST_FAILED.value: (True, False), 68 | CastSpell.errors.CANT_CAST_SPELL.value: (False, True), 69 | CastSpell.errors.NO_FIGHTER_POS.value: (False, True) 70 | } 71 | 72 | can_retry, should_end_turn = spell_retry_map.get(error_code, (False, True)) 73 | 74 | if should_end_turn: 75 | Logger().warning(f"Spell cast failed with non-recoverable error: {error_msg}") 76 | behavior._end_turn(TurnResult.CANNOT_CAST.value, error_msg) 77 | return 78 | 79 | if can_retry and behavior._current_retry_count < 3: 80 | Logger().info(f"Retrying turn after spell cast error: {error_msg}") 81 | behavior._current_retry_count += 1 82 | behavior._action_queue.clear() 83 | behavior.main() 84 | # Restart turn planning 85 | else: 86 | Logger().warning(f"Spell cast failed after retries: {error_msg}") 87 | behavior._end_turn(TurnResult.CANNOT_CAST.value, error_msg) 88 | -------------------------------------------------------------------------------- /pyd2bot/logic/fight/behaviors/fight_turn/spell_utils.py: -------------------------------------------------------------------------------- 1 | from pydofus2.com.ankamagames.atouin.utils.DataMapProvider import DataMapProvider 2 | from typing import TYPE_CHECKING 3 | 4 | from pydofus2.com.ankamagames.dofus.datacenter.spells.Spell import Spell 5 | from pydofus2.com.ankamagames.dofus.logic.game.fight.managers.CurrentPlayedFighterManager import CurrentPlayedFighterManager 6 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 7 | from pydofus2.com.ankamagames.jerakine.types.zones.Cross import Cross 8 | from pydofus2.com.ankamagames.jerakine.types.zones.Lozenge import Lozenge 9 | from pydofus2.com.ankamagames.jerakine.utils.display.spellZone.SpellShapeEnum import SpellShapeEnum 10 | from pydofus2.mapTools import MapTools 11 | from pydofus2.com.ankamagames.dofus.internalDatacenter.spells.SpellWrapper import SpellWrapper 12 | 13 | if TYPE_CHECKING: 14 | from pydofus2.com.ankamagames.jerakine.types.zones.DisplayZone import DisplayZone 15 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 16 | from pyd2bot.data.models import Character 17 | 18 | 19 | def getSpellShape(spellw: "SpellWrapper") -> int: 20 | for spellEffect in spellw["effects"]: 21 | if spellEffect.zoneShape != 0 and ( 22 | spellEffect.zoneSize > 0 23 | or spellEffect.zoneSize == 0 24 | and (spellEffect.zoneShape == SpellShapeEnum.P or spellEffect.zoneMinSize < 0) 25 | ): 26 | return spellEffect.zoneShape 27 | return 0 28 | 29 | def getSpellZone(spellw: "SpellWrapper") -> "DisplayZone": 30 | range = spellw["range"] 31 | minRange = spellw["minRange"] 32 | if range is None or minRange is None: 33 | raise Exception(f"Spell range is None, {minRange} {range}") 34 | if range < minRange: 35 | range = minRange; 36 | spellShape = getSpellShape(spellw) 37 | castInLine = spellw["castInLine"] or (spellShape == SpellShapeEnum.l) 38 | if castInLine: 39 | if spellw["castInDiagonal"]: 40 | return Cross(SpellShapeEnum.UNKNOWN, minRange, range, DataMapProvider(), False, True) 41 | return Cross(SpellShapeEnum.UNKNOWN, minRange, range, DataMapProvider(), False) 42 | elif spellw["castInDiagonal"]: 43 | return Cross(SpellShapeEnum.UNKNOWN, minRange, range, DataMapProvider(), True) 44 | else: 45 | return Lozenge(SpellShapeEnum.UNKNOWN, minRange, range, DataMapProvider()) 46 | 47 | def check_line_of_sight(start_cell_id: int, end_cell_id: int) -> tuple[bool, str]: 48 | """Check if there is line of sight between two cells. 49 | 50 | Args: 51 | start_cell_id: Starting cell ID 52 | end_cell_id: Target cell ID 53 | fighter_pos: Current fighter position (for logging) 54 | 55 | Returns: 56 | Tuple of (has_los, reason) where has_los is True if there is LOS, 57 | and reason explains why if there isn't 58 | """ 59 | line = MapTools.getMpLine(start_cell_id, end_cell_id) 60 | if len(line) <= 1: 61 | return True, "" 62 | 63 | for mp in line[:-1]: 64 | if not DataMapProvider().pointLos(mp.x, mp.y, False): 65 | return False, f"Obstacle between cells {start_cell_id} and {end_cell_id}" 66 | 67 | return True, "" 68 | 69 | def can_cast_spell_on_cell(spellw: "SpellWrapper", target_cell: int=0) -> tuple[bool, str]: 70 | return CurrentPlayedFighterManager().canCastThisSpell(spellw.spellId, spellw.spellLevel, target_cell) 71 | 72 | def get_player_spellw(playerManager: "PlayedCharacterManager", spellId: int, player: "Character") -> "SpellWrapper": 73 | if not playerManager: 74 | Logger().error("Asking for spellw when there is no player manager!") 75 | return None 76 | res = playerManager.getSpellById(spellId) 77 | if not res: 78 | Logger().error( 79 | f"Player {player.name} doesn't have spell list {playerManager.playerSpellList}" 80 | ) 81 | res = SpellWrapper.create(spellId) 82 | spell = Spell.getSpellById(spellId) 83 | currentCharacterLevel = playerManager.limitedLevel 84 | spellLevels = spell.spellLevelsInfo 85 | index = 0 86 | for i in range(len(spellLevels) - 1, -1, -1): 87 | if currentCharacterLevel >= spellLevels[i].minPlayerLevel: 88 | index = i 89 | break 90 | res._spellLevel = spellLevels[index] 91 | res.spellLevel = index + 1 92 | playerManager.playerSpellList.append(res) 93 | return res -------------------------------------------------------------------------------- /pyd2bot/logic/fight/fight_seq_diagram.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | sequenceDiagram 3 | participant Server 4 | participant FightAIFrame 5 | 6 | Note over Server,FightAIFrame: Fight Start Phase 7 | Server->>FightAIFrame: GameFightStartMessage 8 | Server->>FightAIFrame: GameEntitiesDispositionMessage 9 | Server->>FightAIFrame: ChallengeListMessage 10 | Server->>FightAIFrame: GameFightTurnListMessage 11 | Server->>FightAIFrame: GameFightSynchronizeMessage 12 | 13 | Note over Server,FightAIFrame: Round Start 14 | Server->>FightAIFrame: GameFightNewRoundMessage 15 | 16 | Note over Server,FightAIFrame: Turn Sequence 17 | Server->>FightAIFrame: GameFightTurnStartMessage 18 | 19 | Note over Server,FightAIFrame: Sequence Block 20 | Server->>FightAIFrame: SequenceStartMessage 21 | Server->>FightAIFrame: SequenceEndMessage 22 | FightAIFrame-->>Server: GameActionAcknowledgementMessage 23 | 24 | Server->>FightAIFrame: GameFightTurnStartPlayingMessage 25 | 26 | Note over Server,FightAIFrame: Turn End 27 | Server->>FightAIFrame: GameFightTurnReadyRequestMessage 28 | FightAIFrame-->>Server: GameFightTurnReadyMessage 29 | Server->>FightAIFrame: GameFightTurnEndMessage 30 | 31 | Note over Server,FightAIFrame: Next Turn Starts 32 | Server->>FightAIFrame: GameFightTurnStartMessage 33 | 34 | Note over Server,FightAIFrame: Can repeat with more rounds... 35 | Server->>FightAIFrame: GameFightNewRoundMessage 36 | 37 | Note over Server,FightAIFrame: Fight End 38 | Server->>FightAIFrame: GameFightEndMessage 39 | ``` -------------------------------------------------------------------------------- /pyd2bot/logic/fight/frames/FightAIFrame.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.data.models import Session 2 | from pyd2bot.logic.fight.behaviors.FightPreparation import FightPreparation 3 | from pyd2bot.logic.fight.behaviors.fight_turn.FightPlayTurn import FightPlayTurn 4 | from pyd2bot.logic.fight.behaviors.FightStateManager import FightStateManager 5 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 6 | from pydofus2.com.ankamagames.berilia.managers.KernelEventsManager import KernelEventsManager 7 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 8 | from pydofus2.com.ankamagames.dofus.kernel.net.ConnectionsHandler import ConnectionsHandler 9 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 10 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.fight.GameFightEndMessage import GameFightEndMessage 11 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.fight.GameFightTurnReadyMessage import GameFightTurnReadyMessage 12 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.fight.GameFightTurnReadyRequestMessage import GameFightTurnReadyRequestMessage 13 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.fight.GameFightTurnStartMessage import GameFightTurnStartMessage 14 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.fight.GameFightTurnStartPlayingMessage import GameFightTurnStartPlayingMessage 15 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 16 | from pydofus2.com.ankamagames.jerakine.messages.Frame import Frame 17 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 18 | from pydofus2.com.ankamagames.jerakine.types.enums.Priority import Priority 19 | 20 | class FightAIFrame(Frame): 21 | def __init__(self, session: Session): 22 | self.session = session 23 | self.state_manager = FightStateManager() 24 | 25 | super().__init__() 26 | 27 | def pushed(self) -> bool: 28 | Kernel().defer(FightPreparation().start) 29 | return True 30 | 31 | def pulled(self) -> bool: 32 | return True 33 | 34 | @property 35 | def priority(self) -> int: 36 | return Priority.VERY_LOW 37 | 38 | def process(self, msg: Message) -> bool: 39 | """Handle fight protocol messages""" 40 | if isinstance(msg, GameFightTurnStartMessage): 41 | return self._handle_turn_start(msg) 42 | 43 | elif isinstance(msg, GameFightTurnStartPlayingMessage): 44 | return self._handle_turn_playing() 45 | 46 | elif isinstance(msg, GameFightTurnReadyRequestMessage): 47 | return self._handle_ready_request() 48 | 49 | elif isinstance(msg, GameFightEndMessage): 50 | return self._handle_fight_end() 51 | 52 | return False 53 | 54 | def _handle_turn_start(self, msg: GameFightTurnStartMessage) -> bool: 55 | """Process turn start - identify if it's our turn""" 56 | self.state_manager.refresh_turn_state(msg) 57 | 58 | # Identify if it's our party member's turn 59 | player_infos = self.session.getPlayerById(msg.id) 60 | 61 | if player_infos: 62 | self.state_manager.current_player = player_infos 63 | Logger().info(f"It's our player's turn: {player_infos.name}") 64 | # If we already got play signal 65 | if self.state_manager.turn_playing: 66 | self._play_turn() 67 | else: 68 | Logger().info(f"Other player's turn: {msg.id}") 69 | 70 | return True 71 | 72 | def _handle_turn_playing(self) -> bool: 73 | """Handle turn play signal""" 74 | self.state_manager.turn_playing = True 75 | 76 | if self.state_manager.current_player: 77 | self._play_turn() 78 | 79 | return True 80 | 81 | def _handle_ready_request(self) -> bool: 82 | """Handle turn ready request""" 83 | if Kernel().battleFrame.is_sequence_executing(): 84 | Logger().warning("Delaying turn end acknowledgement due to active sequence") 85 | KernelEventsManager().once(KernelEvent.SequenceExecFinished, lambda *_: self._send_ready(), originator=self) 86 | return True 87 | 88 | self._send_ready() 89 | return True 90 | 91 | def _handle_fight_end(self) -> bool: 92 | """Handle fight end cleanup""" 93 | if self.session.followers: 94 | for player in self.session.followers: 95 | player_manager = PlayedCharacterManager.getInstance(player.accountId) 96 | if player_manager: 97 | player_manager.isFighting = False 98 | if FightPreparation().isRunning(): 99 | FightPreparation().stop() 100 | if FightPlayTurn().isRunning(): 101 | FightPlayTurn().stop() 102 | Kernel().worker.removeFrame(self) 103 | return True 104 | 105 | def _play_turn(self): 106 | """Start turn execution if conditions met""" 107 | FightPlayTurn().start() 108 | 109 | def _send_ready(self): 110 | """Send turn ready acknowledgement""" 111 | self.state_manager.cleanup_turn_state() 112 | msg = GameFightTurnReadyMessage() 113 | msg.init(True) 114 | ConnectionsHandler().send(msg) 115 | -------------------------------------------------------------------------------- /pyd2bot/logic/fight/frames/MuleFightFrame.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.data.models import Character 2 | from pyd2bot.logic.fight.messages.MuleSwitchedToCombatContext import \ 3 | MuleSwitchedToCombatContext 4 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 5 | from pydofus2.com.ankamagames.dofus.kernel.net.ConnectionsHandler import \ 6 | ConnectionsHandler 7 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 8 | PlayedCharacterManager 9 | from pydofus2.com.ankamagames.dofus.network.messages.game.actions.fight.GameActionFightNoSpellCastMessage import \ 10 | GameActionFightNoSpellCastMessage 11 | from pydofus2.com.ankamagames.dofus.network.messages.game.basic.TextInformationMessage import \ 12 | TextInformationMessage 13 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.fight.GameFightTurnReadyMessage import \ 14 | GameFightTurnReadyMessage 15 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.fight.GameFightTurnReadyRequestMessage import \ 16 | GameFightTurnReadyRequestMessage 17 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.fight.GameFightTurnStartPlayingMessage import \ 18 | GameFightTurnStartPlayingMessage 19 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.GameContextReadyMessage import \ 20 | GameContextReadyMessage 21 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.GameMapNoMovementMessage import \ 22 | GameMapNoMovementMessage 23 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.roleplay.CurrentMapMessage import \ 24 | CurrentMapMessage 25 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 26 | from pydofus2.com.ankamagames.jerakine.messages.Frame import Frame 27 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 28 | from pydofus2.com.ankamagames.jerakine.types.enums.Priority import Priority 29 | 30 | 31 | class MuleFightFrame(Frame): 32 | 33 | def __init__(self, leader: Character): 34 | super().__init__() 35 | self.leader = leader 36 | 37 | @property 38 | def priority(self) -> int: 39 | return Priority.VERY_LOW 40 | 41 | def pushed(self) -> bool: 42 | Logger().info("BotMuleFightFrame pushed") 43 | Kernel.getInstance(self.leader.accountId).worker.process(MuleSwitchedToCombatContext(PlayedCharacterManager().id)) 44 | return True 45 | 46 | def pulled(self) -> bool: 47 | Logger().info("BotMuleFightFrame pulled") 48 | return True 49 | 50 | def process(self, msg: Message) -> bool: 51 | 52 | if isinstance(msg, GameFightTurnReadyRequestMessage): 53 | turnEnd = GameFightTurnReadyMessage() 54 | turnEnd.init(True) 55 | ConnectionsHandler().send(turnEnd) 56 | return True 57 | 58 | elif isinstance(msg, CurrentMapMessage): 59 | msg = GameContextReadyMessage() 60 | msg.init(int(msg.mapId)) 61 | ConnectionsHandler().send(msg) 62 | return True 63 | 64 | elif isinstance(msg, (GameMapNoMovementMessage, GameActionFightNoSpellCastMessage, GameFightTurnStartPlayingMessage, TextInformationMessage)): 65 | Kernel.getInstance(self.leader.accountId).worker.process(msg) 66 | return True -------------------------------------------------------------------------------- /pyd2bot/logic/fight/frames/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/fight/frames/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/fight/messages/MuleSwitchedToCombatContext.py: -------------------------------------------------------------------------------- 1 | class MuleSwitchedToCombatContext: 2 | 3 | def __init__(self, muleId): 4 | self.muleId = muleId 5 | 6 | def __str__(self) -> str: 7 | return f"MuleSwitchedToCombat({self.muleId})" 8 | -------------------------------------------------------------------------------- /pyd2bot/logic/fight/messages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/fight/messages/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/managers/AccountCharactersFetcher.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.data.models import Account, Character 2 | from pydofus2.com.DofusClient import DofusClient 3 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 4 | from pydofus2.com.ankamagames.berilia.managers.KernelEventsManager import KernelEventsManager 5 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 6 | from pydofus2.com.ankamagames.dofus.logic.common.managers.PlayerManager import PlayerManager 7 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 8 | 9 | 10 | class AccountCharactersFetcher(DofusClient): 11 | 12 | def __init__(self, account: Account, callback=None): 13 | self.account = account 14 | if account.apikey is None: 15 | raise ValueError("Account apikey is required!") 16 | if callback is not None and not callable(callback): 17 | raise ValueError("Callback must be a callable function") 18 | super().__init__(account.id) 19 | self.callback = callback 20 | self.changeServer = False 21 | self.currServer = None 22 | self.serversList = None 23 | self.setCredentials(account.apikey, account.certId, account.certHash) 24 | self.addShutdownListener(self.afterShutDown) 25 | 26 | def afterShutDown(self, reason, message): 27 | Logger().info(f"Characters fetched for account {self.account.login} ended with message : {message} and reason : {reason}") 28 | if self.callback: 29 | if not callable(self.callback): 30 | Logger().error("Callback is not callable", exc_info=True) 31 | return 32 | self.callback(self.account.characters, message, reason) 33 | 34 | def initListeners(self): 35 | super().initListeners() 36 | KernelEventsManager().once( 37 | KernelEvent.ServersList, 38 | self.onServersList, 39 | originator=self, 40 | ) 41 | 42 | def onServersList(self, event, serversList, serversUsedList, serversTypeAvailableSlots): 43 | selectableServers = [server for server in Kernel().serverSelectionFrame.usedServers if server.isSelectable] 44 | self.serversListIter = iter(selectableServers) 45 | self.processServer() 46 | 47 | def onServerSelectionRefused(self, event, serverId, error, serverStatus, error_text, selectableServers): 48 | Logger().error(f"Server {serverId} selection refused for reason : {error_text}") 49 | self.processServer() 50 | 51 | def processServer(self): 52 | try: 53 | self.currServer = next(self.serversListIter) 54 | except StopIteration: 55 | self.shutdown("Wanted shutdown after all servers processed.") 56 | return 57 | if self.changeServer: 58 | PlayerManager().charactersList.clear() 59 | Kernel().characterFrame.changeToServer(self.currServer.id) 60 | else: 61 | self.changeServer = True 62 | Kernel().serverSelectionFrame.selectServer(self.currServer.id) 63 | KernelEventsManager().once(KernelEvent.CharactersList, self.onCharactersList) 64 | 65 | def onCharactersList(self, event, charactersList): 66 | Logger().info(f"Server : {self.currServer.id}, List of characters received") 67 | self.account.characters += [ 68 | Character(** 69 | { 70 | "name": character.name, 71 | "id": character.id, 72 | "level": character.level, 73 | "breedId": character.breedId, 74 | "breedName": character.breed.name, 75 | "serverId": PlayerManager().server.id, 76 | "serverName": PlayerManager().server.name, 77 | "accountId": self.account.id, 78 | } 79 | ) 80 | for character in PlayerManager().charactersList 81 | ] 82 | self.processServer() 83 | -------------------------------------------------------------------------------- /pyd2bot/logic/managers/AccountManager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from pyd2bot.logic.managers.AccountCharactersFetcher import AccountCharactersFetcher 5 | from pyd2bot.data.models import Account, Character, Credentials 6 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 7 | from pydofus2.Zaap.ZaapDecoy import ZaapDecoy 8 | 9 | __dir__ = os.path.dirname(os.path.abspath(__file__)) 10 | default_accounts_json_file = os.path.join(__dir__, "accounts.json") 11 | 12 | 13 | class AccountManager: 14 | _zaap: ZaapDecoy = None 15 | _accounts = dict[int, Account]() 16 | _db_file = default_accounts_json_file 17 | 18 | @classmethod 19 | def get_account(cls, accountId) -> Account: 20 | if accountId not in cls._accounts: 21 | print(cls._accounts) 22 | raise Exception(f"Account {accountId} not found") 23 | return cls._accounts[accountId] 24 | 25 | @classmethod 26 | def get_accounts(cls) -> list[Account]: 27 | return cls._accounts.values() 28 | 29 | @classmethod 30 | def get_credentials(cls, accountId) -> Credentials: 31 | return cls.get_account(accountId).credentials 32 | 33 | @classmethod 34 | def get_character(cls, accountId, charId=None) -> Character: 35 | account = cls.get_account(accountId) 36 | return account.get_character(charId) 37 | 38 | @classmethod 39 | def get_apikey(cls, accountId): 40 | return cls.get_account(accountId).apikey 41 | 42 | @classmethod 43 | def fetch_account(cls, apikey, certid=0, certhash="") -> Account: 44 | Logger().debug(f"Fetching account data") 45 | if not cls._zaap: 46 | cls._zaap = ZaapDecoy(apikey) 47 | accountData = cls._zaap.mainAccount 48 | else: 49 | accountData = ZaapDecoy().fetch_account_data(apikey) 50 | account = Account(**accountData["account"]) 51 | account.apikey = apikey 52 | account.certId = certid 53 | account.certHash = certhash 54 | return account 55 | 56 | @classmethod 57 | def fetch_characters_async(cls, accountId, callback) -> AccountCharactersFetcher: 58 | account = cls.get_account(accountId) 59 | fetcher = AccountCharactersFetcher(account, callback) 60 | fetcher.start() 61 | return fetcher 62 | 63 | @classmethod 64 | def fetch_characters_sync(cls, accountId) -> list[Character]: 65 | account = cls.get_account(accountId) 66 | fetcher = AccountCharactersFetcher(account) 67 | fetcher.start() 68 | fetcher.join() 69 | return account.characters 70 | 71 | @classmethod 72 | def load(cls, local_json_file=None): 73 | if local_json_file is not None: 74 | cls._db_file = local_json_file 75 | if not cls._db_file: 76 | cls._db_file = default_accounts_json_file 77 | if not os.path.exists(cls._db_file): 78 | return 79 | with open(cls._db_file, "r") as fp: 80 | accounts_json = json.load(fp) 81 | cls._accounts = { 82 | int(accountId): Account(**account) for accountId, account in accounts_json.items() 83 | } 84 | 85 | @classmethod 86 | def save(cls, local_json_file=None): 87 | if local_json_file is None: 88 | cls._db_file = default_accounts_json_file 89 | if not cls._db_file: 90 | cls._db_file = local_json_file 91 | with open(cls._db_file, "w") as fp: 92 | accounts_json = {accountId: account.model_dump() for accountId, account in cls._accounts.items()} 93 | json.dump(accounts_json, fp, indent=4) 94 | 95 | @classmethod 96 | def clear(cls, persist=False): 97 | cls._accounts = {} 98 | if persist: 99 | cls.save() 100 | 101 | @classmethod 102 | def import_launcher_accounts(cls, fetch_characters=False, save_to_local_json=False): 103 | cls._zaap = ZaapDecoy() 104 | cls._accounts.clear() 105 | for apikey in cls._zaap._api_keys: 106 | try: 107 | cert = cls._zaap.get_api_cert(apikey) 108 | account = AccountManager.fetch_account(apikey.key, cert.id, cert.hash) 109 | cls._accounts[account.id] = account 110 | except Exception as exc: 111 | Logger().error(f"Failed to fetch characters from game server:\n{exc}", exc_info=True) 112 | if fetch_characters: 113 | tasks = [AccountCharactersFetcher(account) for account in cls._accounts.values()] 114 | for task in tasks: 115 | task.start() 116 | for task in tasks: 117 | task.join() 118 | if save_to_local_json: 119 | cls.save(save_to_local_json) 120 | return cls._accounts 121 | 122 | 123 | if __name__ == "__main__": 124 | Logger.logToConsole = True 125 | AccountManager.clear() 126 | AccountManager.import_launcher_accounts(fetch_characters=True, save_to_local_json=True) 127 | -------------------------------------------------------------------------------- /pyd2bot/logic/managers/PathFactory.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.farmPaths.CustomRandomFarmPath import CustomRandomFarmPath 2 | from pyd2bot.farmPaths.CyclicFarmPath import CyclicFarmPath 3 | from pyd2bot.farmPaths.RandomAreaFarmPath import RandomAreaFarmPath 4 | from pyd2bot.farmPaths.RandomSubAreaFarmPath import \ 5 | RandomSubAreaFarmPath 6 | 7 | from pyd2bot.data.enums import PathTypeEnum 8 | from typing import TYPE_CHECKING 9 | 10 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.WorldGraph import WorldGraph 11 | 12 | if TYPE_CHECKING: 13 | from pyd2bot.data.models import Path 14 | 15 | class PathFactory: 16 | 17 | @classmethod 18 | def from_dto(cls, obj: 'Path'): 19 | from pyd2bot.data.models import Path 20 | 21 | if not isinstance(obj, Path): 22 | raise ValueError("session.path must be a Path instance, not " + str(type(obj))) 23 | 24 | if obj.type == PathTypeEnum.RandomSubAreaFarmPath: 25 | return RandomSubAreaFarmPath( 26 | name=obj.id, 27 | startVertex=WorldGraph().getVertex(obj.startMapId, obj.startZoneId), 28 | allowedTransitions=obj.allowedTransitions, 29 | ) 30 | 31 | if obj.type == PathTypeEnum.RandomAreaFarmPath: 32 | return RandomAreaFarmPath( 33 | name=obj.id, 34 | startVertex=WorldGraph().getVertex(obj.startMapId, obj.startZoneId) 35 | ) 36 | 37 | if obj.type == PathTypeEnum.CustomRandomFarmPath: 38 | return CustomRandomFarmPath( 39 | name=obj.id, 40 | mapIds=obj.mapIds, 41 | ) 42 | 43 | if obj.type == PathTypeEnum.CyclicFarmPath: 44 | return CyclicFarmPath( 45 | name=obj.id, 46 | mapIds=obj.mapIds, 47 | ) 48 | 49 | raise ValueError("Unknown path type: " + str(obj.type)) 50 | -------------------------------------------------------------------------------- /pyd2bot/logic/managers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/managers/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/TreePrinter.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 3 | 4 | 5 | class TreePrinter: 6 | CHARS = { 7 | 'corner': '\\--', 8 | 'tee': '+--', 9 | 'vertical': '| ', 10 | 'space': ' ' 11 | } 12 | 13 | @staticmethod 14 | def get_ascii_tree(node, prefix="", is_last=True, include_root=True, visited=None): 15 | """ 16 | Returns a string representation of the tree using ASCII characters. 17 | 18 | Args: 19 | node: The node to print (must have children attribute and a __str__ method) 20 | prefix (str): Current prefix for line (used in recursion) 21 | is_last (bool): Is this the last child of its parent? 22 | include_root (bool): Whether to include the root node in the output 23 | visited (set): Set of visited node ids to detect cycles 24 | 25 | Returns: 26 | str: ASCII representation of the tree 27 | """ 28 | if visited is None: 29 | visited = set() 30 | 31 | node_id = id(node) 32 | result = [] 33 | 34 | # Check for cycles 35 | if node_id in visited: 36 | return f"{prefix}{TreePrinter.CHARS['corner']}[CYCLE] {type(node).__name__}" 37 | 38 | visited.add(node_id) 39 | 40 | # Add current node 41 | if not include_root and prefix == "": 42 | pass # Skip root 43 | else: 44 | node_char = TreePrinter.CHARS['corner'] if is_last else TreePrinter.CHARS['tee'] 45 | result.append(f"{prefix}{node_char}{type(node).__name__}") 46 | 47 | # Prepare prefix for children 48 | child_prefix = prefix + (TreePrinter.CHARS['space'] if is_last else TreePrinter.CHARS['vertical']) 49 | 50 | # Process children 51 | if hasattr(node, 'children'): 52 | children = node.children 53 | for i, child in enumerate(children): 54 | if child == node: 55 | Logger().error(f"node {type(node).__name__} is its own child") 56 | continue 57 | is_last_child = i == len(children) - 1 58 | result.append(TreePrinter.get_ascii_tree( 59 | child, 60 | child_prefix, 61 | is_last_child, 62 | include_root=True, 63 | visited=visited.copy() # Pass a copy to avoid affecting sibling traversal 64 | )) 65 | 66 | return "\n".join(result) 67 | 68 | @staticmethod 69 | def get_compact_tree(node, level=0, include_root=True, visited=None): 70 | """ 71 | Returns a compact string representation of the tree using simple indentation. 72 | """ 73 | if visited is None: 74 | visited = set() 75 | 76 | node_id = id(node) 77 | if node_id in visited: 78 | return f"{' ' * level}[CYCLE] {type(node).__name__}" 79 | 80 | visited.add(node_id) 81 | result = [] 82 | indent = " " * level 83 | 84 | if not (level == 0 and not include_root): 85 | result.append(f"{indent}{type(node).__name__}") 86 | 87 | if hasattr(node, 'children'): 88 | for child in node.children: 89 | result.append(TreePrinter.get_compact_tree( 90 | child, 91 | level + 1, 92 | include_root=True, 93 | visited=visited.copy() 94 | )) 95 | 96 | return "\n".join(result) 97 | 98 | @staticmethod 99 | def get_detailed_tree(node, level=0, include_root=True, show_attributes=False, visited=None): 100 | """ 101 | Returns a detailed string representation of the tree including node attributes. 102 | """ 103 | if visited is None: 104 | visited = set() 105 | 106 | node_id = id(node) 107 | if node_id in visited: 108 | return f"{' ' * level}[CYCLE] {type(node).__name__}" 109 | 110 | visited.add(node_id) 111 | result = [] 112 | indent = " " * level 113 | 114 | if not (level == 0 and not include_root): 115 | node_str = type(node).__name__ 116 | if show_attributes: 117 | attrs = {k: v for k, v in vars(node).items() 118 | if not k.startswith('_') and k != 'children'} 119 | if attrs: 120 | node_str += f" {attrs}" 121 | result.append(f"{indent}{node_str}") 122 | 123 | if hasattr(node, 'children'): 124 | for child in node.children: 125 | result.append(TreePrinter.get_detailed_tree( 126 | child, 127 | level + 1, 128 | include_root=True, 129 | show_attributes=show_attributes, 130 | visited=visited.copy() 131 | )) 132 | 133 | return "\n".join(result) 134 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bank/OpenBank.py: -------------------------------------------------------------------------------- 1 | 2 | from pyd2bot.data.enums import ServerNotificationEnum 3 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 4 | from pyd2bot.misc.Localizer import BankInfos, Localizer 5 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 6 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 7 | PlayedCharacterManager 8 | from pydofus2.com.ankamagames.dofus.network.enums.ExchangeTypeEnum import \ 9 | ExchangeTypeEnum 10 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 11 | 12 | 13 | class OpenBank(AbstractBehavior): 14 | STORAGE_OPEN_TIMED_OUT = 9874521 15 | WRONG_EXCHANGE_TYPE = 556988 16 | INSUFFICIENT_KAMAS = 3258253 17 | ERROR_BANK_NOT_FOUND = 32582543 18 | 19 | def __init__(self, return_to_start=None): 20 | super().__init__() 21 | self.return_to_start = return_to_start 22 | 23 | def _on_search_nearest_bank_storage(self, code, error, path, infos: BankInfos): 24 | if path is None or error: 25 | return self.finish(self.ERROR_BANK_NOT_FOUND, f"No accessible bank storage found, search ended with result error[{code}] {error}") 26 | 27 | self.infos = infos 28 | self.path_to_bank = path 29 | self._on_bank_infos() 30 | 31 | def run(self, bankInfos: BankInfos=None, path_to_bank=None) -> bool: 32 | if bankInfos is None: 33 | return Localizer.findClosestBankAsync(callback=self._on_search_nearest_bank_storage) 34 | 35 | self.infos = bankInfos 36 | self.path_to_bank = path_to_bank 37 | self._on_bank_infos() 38 | 39 | def _on_bank_infos(self): 40 | Logger().debug("Bank infos: %s", self.infos.__dict__) 41 | self._startMapId = PlayedCharacterManager().currentMap.mapId 42 | self._startRpZone = PlayedCharacterManager().currentZoneRp 43 | self.on(KernelEvent.ServerTextInfo, self.onTextInformation) 44 | 45 | self.npc_dialog( 46 | self.infos.npcMapId, 47 | self.infos.npcId, 48 | self.infos.npcActionId, 49 | self.infos.questionsReplies, 50 | path_to_npc=self.path_to_bank, 51 | callback=self.onBankManDialogEnded, 52 | ) 53 | 54 | def onTextInformation(self, event, msgId, msgType, textId, msgContent, params): 55 | if textId == ServerNotificationEnum.NOT_ENOUGH_KAMAS: 56 | self.finish(self.INSUFFICIENT_KAMAS, "Insufficient kamas to open bank") 57 | elif textId == ServerNotificationEnum.KAMAS_LOST: 58 | self.send(KernelEvent.KamasLostFromBankOpen, int(params[0])) 59 | elif textId == ServerNotificationEnum.BANK_OPEN_TAX: 60 | self.send(KernelEvent.KamasLostFromBankOpen, int(params[0])) 61 | 62 | def onBankManDialogEnded(self, code, error): 63 | if error: 64 | return self.finish(code, error) 65 | 66 | Logger().info("Ended bank man dialog waiting for storage to open...") 67 | self.once( 68 | event_id=KernelEvent.ExchangeBankStartedWithStorage, 69 | callback=self.onStorageOpen, 70 | timeout=30, 71 | ontimeout=lambda: self.finish(self.STORAGE_OPEN_TIMED_OUT, "Dialog with Bank NPC ended correctly but storage didnt open on time!"), 72 | ) 73 | 74 | def onBankContent(self, event, objects, kamas): 75 | self.finish(0) 76 | 77 | def onStorageOpen(self, event, exchangeType, pods): 78 | if exchangeType == ExchangeTypeEnum.BANK: 79 | Logger().info("Bank storage opened") 80 | self.once(KernelEvent.InventoryContent, self.onBankContent) 81 | else: 82 | self.finish(self.WRONG_EXCHANGE_TYPE ,f"Expected BANK storage to open but another type of exchange '{ExchangeTypeEnum.BANK}'!") 83 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bank/RetrieveKamasFromBank.py: -------------------------------------------------------------------------------- 1 | from typing import Set 2 | 3 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 4 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 5 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 6 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.InventoryManager import InventoryManager 7 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 8 | from pydofus2.com.ankamagames.dofus.network.messages.game.inventory.KamasUpdateMessage import KamasUpdateMessage 9 | from pydofus2.com.ankamagames.dofus.network.messages.game.inventory.exchanges.ExchangeBidHouseItemAddOkMessage import ExchangeBidHouseItemAddOkMessage 10 | from pydofus2.com.ankamagames.dofus.network.messages.game.inventory.items.ObjectDeletedMessage import ObjectDeletedMessage 11 | from pydofus2.com.ankamagames.dofus.network.messages.game.inventory.items.ObjectQuantityMessage import ObjectQuantityMessage 12 | from pydofus2.com.ankamagames.dofus.network.messages.game.inventory.items.InventoryWeightMessage import InventoryWeightMessage 13 | from pydofus2.com.ankamagames.dofus.network.messages.game.inventory.storage.StorageKamasUpdateMessage import StorageKamasUpdateMessage 14 | 15 | class RetrieveKamasFromBank(AbstractBehavior): 16 | """Handles the async logic for selling a single item in the marketplace""" 17 | REQUIRED_SEQUENCE = { 18 | "kamas_bank", 19 | "kamas_inventory", 20 | } 21 | 22 | def __init__(self): 23 | super().__init__() 24 | self._received_sequence: Set[str] = set() 25 | 26 | def run(self) -> bool: 27 | """Start the sell operation""" 28 | bank_kamas = InventoryManager().bankInventory.kamas 29 | if bank_kamas <= 0: 30 | return self.finish(0) 31 | self.once(KernelEvent.MessageReceived, self._process_message) 32 | Kernel().exchangeManagementFrame.exchangeObjectMoveKama(bank_kamas) 33 | 34 | def _process_message(self, event, msg) -> None: 35 | """Track complete server response sequence""" 36 | if isinstance(msg, KamasUpdateMessage): 37 | self._received_sequence.add("kamas_inventory") 38 | 39 | elif isinstance(msg, StorageKamasUpdateMessage): 40 | self._received_sequence.add("kamas_bank") 41 | 42 | # Check if sequence is complete 43 | if self._received_sequence >= self.REQUIRED_SEQUENCE: 44 | self._received_sequence.clear() 45 | self.finish(0) 46 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bank/RetrieveRecipeFromBank.py: -------------------------------------------------------------------------------- 1 | 2 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 3 | from pyd2bot.misc.Localizer import Localizer 4 | from pydofus2.Ankama_Common.ui.Recipes import Recipes 5 | from pydofus2.Ankama_storage.ui.enum.StorageState import StorageState 6 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 7 | from pydofus2.com.ankamagames.dofus.datacenter.jobs.Recipe import Recipe 8 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 9 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 10 | PlayedCharacterManager 11 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 12 | 13 | 14 | class RetrieveRecipeFromBank(AbstractBehavior): 15 | BANK_CLOSE_TIMED_OUT = 89987 16 | RETRIEVE_ITEMS_TIMED_OUT = 998877 17 | 18 | def __init__(self): 19 | super().__init__() 20 | self.return_to_start = None 21 | 22 | def run(self, recipe: Recipe, return_to_start=True, bankInfos=None) -> bool: 23 | self.recipe = recipe 24 | self.return_to_start = return_to_start 25 | if bankInfos is None: 26 | self.infos = Localizer.getBankInfos() 27 | else: 28 | self.infos = bankInfos 29 | Logger().debug("Bank infos: %s", self.infos.__dict__) 30 | self._startMapId = PlayedCharacterManager().currentMap.mapId 31 | self._startRpZone = PlayedCharacterManager().currentZoneRp 32 | self.recipesUi = Recipes() 33 | self.open_bank(self.infos, callback=self.onStorageOpen) 34 | 35 | def onBankContent(self, event, objects, kamas): 36 | self.recipesUi.load(storage=StorageState.BANK_MOD, uiName="storage") 37 | self.ids, self.qtys = self.recipesUi.calculateIngredientsToRetrieve(self.recipe) 38 | self.recipesUi.unload() 39 | self.once( 40 | KernelEvent.InventoryWeightUpdate, 41 | self.onInventoryWeightUpdate, 42 | timeout=15, 43 | retry_nbr=5, 44 | retry_action=self.pullItems, 45 | ontimeout=lambda: self.finish(self.RETRIEVE_ITEMS_TIMED_OUT, "Pull items from bank storage timeout"), 46 | ) 47 | self.pullItems() 48 | Logger().info("Pull items request sent") 49 | 50 | def onStorageOpen(self, code, err): 51 | if err: 52 | return self.finish(code, err) 53 | self.once( 54 | KernelEvent.InventoryContent, 55 | self.onBankContent, 56 | ) 57 | 58 | def onStorageClose(self, event, success): 59 | Logger().info("Bank storage closed") 60 | if self.return_to_start: 61 | Logger().info(f"Returning to start point") 62 | self.autoTrip(self._startMapId, self._startRpZone, callback=self.finish) 63 | else: 64 | self.finish(0) 65 | 66 | def onInventoryWeightUpdate(self, event, lastWeight, weight, max): 67 | Logger().info(f"Inventory weight percent changed to : {round(100 * weight / max, 1)}%") 68 | self.once( 69 | event_id=KernelEvent.ExchangeClose, 70 | callback=self.onStorageClose, 71 | timeout=10, 72 | ontimeout=lambda: self.finish(self.BANK_CLOSE_TIMED_OUT, "Bank close timed out!"), 73 | retry_nbr=5, 74 | retry_action=Kernel().commonExchangeManagementFrame.leaveShopStock, 75 | ) 76 | Kernel().commonExchangeManagementFrame.leaveShopStock() 77 | 78 | def pullItems(self): 79 | Kernel().exchangeManagementFrame.exchangeObjectTransferListWithQuantityToInv(self.ids, self.qtys) 80 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bank/UnloadInBank.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 3 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 4 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 5 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.InventoryManager import InventoryManager 6 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 7 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 8 | 9 | 10 | class UnloadInBank(AbstractBehavior): 11 | TRANSFER_ITEMS_TIMED_OUT = 111111 12 | BANK_CLOSE_TIMED_OUT = 222222 13 | LEVEL_TOO_LOW = 909799 14 | 15 | def __init__(self): 16 | super().__init__() 17 | self._logger = Logger() 18 | self.return_to_start = None 19 | self._start_map_id = None 20 | self._start_zone = None 21 | self.leave_bank_open = False 22 | 23 | def run(self, return_to_start=True, bankInfos=None, leave_bank_open=False, items_gid_to_keep=None) -> bool: 24 | if PlayedCharacterManager().limitedLevel < 10: 25 | return self.finish(self.LEVEL_TOO_LOW, "Character level is too low to use bank.") 26 | 27 | self.return_to_start = return_to_start 28 | self.leave_bank_open = leave_bank_open 29 | self._start_map_id = PlayedCharacterManager().currentMap.mapId 30 | self._start_zone = PlayedCharacterManager().currentZoneRp 31 | self.items_gid_to_keep = items_gid_to_keep 32 | 33 | # Use open_bank helper 34 | self.open_bank(bankInfos=bankInfos, callback=self._on_bank_opened) 35 | return True 36 | 37 | def _find_items_to_retrieve(self) -> List[Tuple[int, int]]: 38 | if not self.items_gid_to_keep: 39 | return [] 40 | 41 | items_to_retrieve = [] 42 | bank_item_stacks = InventoryManager().bankInventory.getView("bank").content 43 | 44 | for stack in bank_item_stacks: 45 | if stack.objectGID in self.items_gid_to_keep: 46 | items_to_retrieve.append((stack.objectUID, stack.quantity)) 47 | 48 | return items_to_retrieve 49 | 50 | def _on_bank_opened(self, code, error): 51 | if error: 52 | return self.finish(code, error) 53 | 54 | bag_items = InventoryManager().inventory.getView("storage").content 55 | has_items_to_transfer = any(not item.linked for item in bag_items) 56 | 57 | if not has_items_to_transfer: 58 | self._logger.info("No items to unload in bank") 59 | if self.items_gid_to_keep: 60 | items_to_retrieve = self._find_items_to_retrieve() 61 | if items_to_retrieve: 62 | self.pull_bank_items( 63 | [uid for uid, _ in items_to_retrieve], 64 | [qty for _, qty in items_to_retrieve], 65 | lambda: self.close_dialog(self._on_storage_close) if not self.leave_bank_open else self.finish(0) 66 | ) 67 | return 68 | 69 | if self.leave_bank_open: 70 | return self.finish(0) 71 | self.close_dialog(self._on_storage_close) 72 | return 73 | 74 | self.once( 75 | event_id=KernelEvent.InventoryWeightUpdate, 76 | callback=self._on_inventory_weight_update, 77 | timeout=10, 78 | retry_nbr=5, 79 | retry_action=Kernel().exchangeManagementFrame.exchangeObjectTransferAllFromInv, 80 | ontimeout=lambda: self.finish(self.TRANSFER_ITEMS_TIMED_OUT, "Transfer items to bank storage timeout."), 81 | ) 82 | 83 | Kernel().exchangeManagementFrame.exchangeObjectTransferAllFromInv() 84 | self._logger.info("Unload items in bank request sent.") 85 | 86 | def _on_storage_close(self, code, error): 87 | if error: 88 | return self.finish(code, f"Bank close failed with error : {error}") 89 | self._logger.info("Bank storage closed") 90 | 91 | if self.return_to_start: 92 | self._logger.info(f"Returning to start point") 93 | self.autoTrip(self._start_map_id, self._start_zone, callback=self.finish) 94 | else: 95 | self.finish(0, None) 96 | 97 | def _on_inventory_weight_update(self, event, lastWeight, weight, max): 98 | self._logger.info(f"Inventory Weight percent changed to : {round(100 * weight / max, 1)}%") 99 | 100 | if self.items_gid_to_keep: 101 | items_to_retrieve = self._find_items_to_retrieve() 102 | if items_to_retrieve: 103 | self.pull_bank_items( 104 | [uid for uid, _ in items_to_retrieve], 105 | [qty for _, qty in items_to_retrieve], 106 | lambda: self.close_dialog(self._on_storage_close) if not self.leave_bank_open else self.finish(0, None) 107 | ) 108 | return 109 | 110 | if self.leave_bank_open: 111 | return self.finish(0, None) 112 | self.close_dialog(self._on_storage_close) -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bank/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/bank/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bank/retrieval_utils.py: -------------------------------------------------------------------------------- 1 | from typing import List, Set, Tuple 2 | from pydofus2.com.ankamagames.dofus.internalDatacenter.items.ItemWrapper import ItemWrapper 3 | 4 | BATCH_SIZES = [1, 10, 100] 5 | 6 | def find_items_to_retrieve( 7 | server_id: int, 8 | type_ids: Set[int], 9 | bank_items: List[ItemWrapper], 10 | max_pods: int, 11 | max_slots: int, 12 | scorer 13 | ) -> Tuple[List[Tuple[int, int, int]], bool]: # Returns (selections, has_remainder) 14 | candidates = [] 15 | for item in bank_items: 16 | if item.typeId not in type_ids: 17 | continue 18 | 19 | for batch_size in BATCH_SIZES: 20 | if batch_size > item.quantity: 21 | continue 22 | 23 | score = scorer.score(server_id, item.objectGID, batch_size) 24 | candidates.append({ 25 | 'uid': item.objectUID, 26 | 'batch_size': batch_size, 27 | 'available_qty': item.quantity, 28 | 'weight_per_unit': item.weight, 29 | 'score_per_batch': score 30 | }) 31 | 32 | candidates.sort(key=lambda x: x['score_per_batch'], reverse=True) 33 | 34 | selected = [] 35 | remaining_pods = max_pods 36 | remaining_slots = max_slots 37 | has_remainder = False 38 | 39 | for candidate in candidates: 40 | if remaining_slots <= 0 or remaining_pods <= 0: 41 | has_remainder = True 42 | break 43 | 44 | available_batches = candidate['available_qty'] // candidate['batch_size'] 45 | max_by_weight = (remaining_pods // candidate['weight_per_unit']) // candidate['batch_size'] if candidate['weight_per_unit'] > 0 else available_batches 46 | max_by_slots = remaining_slots 47 | 48 | possible_batches = min(available_batches, max_by_weight, max_by_slots) 49 | 50 | if possible_batches > 0: 51 | quantity = possible_batches * candidate['batch_size'] 52 | 53 | # If we can't take all available batches, we have remainder 54 | if possible_batches < available_batches: 55 | has_remainder = True 56 | 57 | selected.append(( 58 | candidate['uid'], 59 | quantity, 60 | candidate['batch_size'] 61 | )) 62 | 63 | remaining_pods -= quantity * candidate['weight_per_unit'] 64 | remaining_slots -= possible_batches 65 | 66 | return selected, has_remainder -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bank/scoring.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Any, Optional 3 | from pyd2bot.logic.roleplay.behaviors.bidhouse.MarketItemAnalytics import MarketStats, MarketItemAnalytics 4 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 5 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 6 | 7 | class MarketScorer: 8 | def __init__(self, stats_max_age_hours: int = 24): 9 | self.analytics = MarketItemAnalytics() 10 | self.stats_max_age_hours = stats_max_age_hours 11 | self._stats_cache = dict() 12 | 13 | def _get_latest_stats(self, server_id: int, gid: int, batch_size: int) -> Optional[MarketStats]: 14 | """Retrieve the latest stats for an item from the database.""" 15 | cache_key = (server_id, gid, batch_size) 16 | 17 | # Check memory cache first 18 | cached = self._stats_cache.get(cache_key) 19 | if cached: 20 | timestamp, stats = cached 21 | if datetime.now() - timestamp <= timedelta(hours=self.stats_max_age_hours): 22 | return stats 23 | 24 | # If not in cache or expired, get from database 25 | try: 26 | with self.analytics.market_db.get_connection() as conn: 27 | with conn.cursor() as cur: 28 | cur.execute(""" 29 | SELECT * 30 | FROM market_statistics 31 | WHERE server_id = %s AND object_gid = %s AND batch_size = %s 32 | ORDER BY calculated_at DESC 33 | LIMIT 1 34 | """, (server_id, gid, batch_size)) 35 | 36 | row = cur.fetchone() 37 | if not row: 38 | return None 39 | 40 | stats = MarketStats( 41 | server_id=row[0], 42 | object_gid=row[1], 43 | batch_size=row[2], 44 | item_name=row[3], 45 | calculated_at=row[4], 46 | num_samples=row[5], 47 | mean_time_to_sell=row[6], 48 | std_time_to_sell=row[7], 49 | exp_rate=row[8], 50 | mean_price=row[9], 51 | std_price=row[10], 52 | median_price=row[11], 53 | mean_tax=row[12], 54 | std_tax=row[13], 55 | sales_rate=row[14], 56 | mean_profit_per_hour=row[15], 57 | std_profit_per_hour=row[16], 58 | p95_profit_per_hour=row[17] 59 | ) 60 | 61 | # Update cache 62 | self._stats_cache[cache_key] = (datetime.now(), stats) 63 | return stats 64 | 65 | except Exception as e: 66 | print(f"Error retrieving stats for item {gid}: {str(e)}") 67 | return None 68 | 69 | def score(self, server_id: int, gid: int, batch_size: int) -> float: 70 | """Score an item based on its market statistics.""" 71 | try: 72 | # Get latest stats (from cache or DB) 73 | stats = self._get_latest_stats(server_id, gid, batch_size) 74 | 75 | # Calculate if missing or too old 76 | if not stats or (datetime.now() - stats.calculated_at) > timedelta(hours=self.stats_max_age_hours): 77 | stats = self.analytics.calculate_item_stats(server_id, gid, batch_size) 78 | if stats: 79 | # Save to DB 80 | self.analytics.batch_save_stats([stats]) 81 | # Update cache 82 | self._stats_cache[(server_id, gid, batch_size)] = (datetime.now(), stats) 83 | 84 | if stats: 85 | return stats.mean_profit_per_hour 86 | 87 | # Fallback calculation if no stats 88 | avg_price = Kernel().averagePricesFrame.getItemAveragePrice(gid) 89 | return (batch_size * avg_price) / 0.5 if avg_price else 0.0 90 | 91 | except Exception as e: 92 | Logger().error(f"Error scoring item {gid}: {str(e)}") 93 | return 0.0 -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bank/statistical_analysis/sheet1.py: -------------------------------------------------------------------------------- 1 | 2 | from pyd2bot.logic.roleplay.behaviors.bidhouse.MarketPersistence import MarketPersistence 3 | from pydofus2.com.ankamagames.dofus.datacenter.items.Item import Item 4 | 5 | def get_avg_tax(server_id: int, gid: int, batch_size: int): 6 | with MarketPersistence().get_connection() as conn: 7 | with conn.cursor() as cur: 8 | cur.execute(""" 9 | SELECT AVG(COALESCE(tax_amount, price * 0.02)) as avg_tax 10 | FROM bids b 11 | LEFT JOIN tax_history t ON 12 | t.object_gid = b.object_gid 13 | AND t.batch_size = b.batch_size 14 | AND t.server_id = b.server_id 15 | WHERE b.object_gid = %s 16 | AND b.server_id = %s 17 | AND b.batch_size = %s 18 | AND b.sold_at IS NOT NULL 19 | """, (gid, server_id, batch_size)) 20 | 21 | result = cur.fetchone() 22 | return float(result[0]) if result[0] else None 23 | 24 | def get_raw_sales(server_id: int, gid: int, batch_size: int): 25 | with MarketPersistence().get_connection() as conn: 26 | with conn.cursor() as cur: 27 | cur.execute(""" 28 | SELECT 29 | price, 30 | sold_at, 31 | created_at 32 | FROM bids 33 | WHERE object_gid = %s 34 | AND server_id = %s 35 | AND batch_size = %s 36 | AND sold_at IS NOT NULL 37 | ORDER BY sold_at DESC 38 | """, (gid, server_id, batch_size)) 39 | 40 | rows = cur.fetchall() 41 | print(f"Found {len(rows)} sales records") 42 | 43 | return [ 44 | { 45 | 'price': float(row[0]), 46 | 'sold_at': row[1], 47 | 'created_at': row[2] 48 | } 49 | for row in rows 50 | ] 51 | 52 | if __name__ == "__main__": 53 | import matplotlib.pyplot as plt 54 | import seaborn as sns 55 | import numpy as np 56 | 57 | server_id = 291 58 | gid = 441 59 | batch_size = 100 60 | 61 | for gid in [312, 313, 350, 395, 421, 428, 441, 442, 443, 444, 445, 446, 449, 461, 441, 16488, 7033, 7032, 6902, 6903, 6897, 2540, 2357, 1782, 474, 471]: 62 | print("\n\n=================================================================================================") 63 | item = Item.getItemById(gid) 64 | 65 | # Get average tax 66 | avg_tax = get_avg_tax(server_id, gid, batch_size) 67 | if not avg_tax: 68 | continue 69 | print(f"Item name : {item.name}") 70 | print(f"Average tax: {avg_tax:,.0f}") 71 | 72 | # Get sales data 73 | sales = get_raw_sales(server_id, gid, batch_size) 74 | 75 | # Calculate profits per hour 76 | profits = [] 77 | for sale in sales: 78 | hours = (sale['sold_at'] - sale['created_at']).total_seconds() / 3600 79 | if hours > 0: # Avoid division by zero 80 | profit_per_hour = (sale['price'] - avg_tax) / hours 81 | profits.append(profit_per_hour) 82 | 83 | if profits: 84 | # Calculate stats 85 | mean_profit = sum(profits) / len(profits) 86 | median_profit = sorted(profits)[len(profits)//2] 87 | percentile_95 = np.percentile(profits, 95) 88 | std_profit = np.std(profits) 89 | 90 | # Print stats 91 | print(f"\nStats:") 92 | print(f"Number of sales: {len(profits)}") 93 | print(f"Mean profit/hour: {mean_profit:,.0f}") 94 | print(f"Median profit/hour: {median_profit:,.0f}") 95 | print(f"95th percentile: {percentile_95:,.0f}") 96 | print(f"Std dev: {std_profit:,.0f}") 97 | print(f"Min profit/hour: {min(profits):,.0f}") 98 | print(f"Max profit/hour: {max(profits):,.0f}") 99 | 100 | # Print distribution by ranges 101 | ranges = [0, 10000, 50000, 100000, float('inf')] 102 | print("\nDistribution by ranges:") 103 | for i in range(len(ranges)-1): 104 | count = sum(1 for p in profits if ranges[i] <= p < ranges[i+1]) 105 | pct = (count / len(profits)) * 100 106 | print(f"{ranges[i]:,} to {ranges[i+1]:,}: {count} sales ({pct:.1f}%)") 107 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bidhouse/EditBidPrice.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Set, Optional 3 | 4 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 5 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 6 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 7 | from pydofus2.com.ankamagames.dofus.logic.common.managers.MarketBid import MarketBid 8 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 9 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 10 | 11 | class EditBidPrice(AbstractBehavior): 12 | """Handles updating a single market listing price""" 13 | 14 | REQUIRED_SEQUENCE = { 15 | "KamasUpdateMessage", 16 | "ExchangeBidHouseItemRemoveOkMessage", 17 | "ExchangeBidHouseItemAddOkMessage" 18 | } 19 | 20 | class ERROR_CODES(Enum): 21 | INSUFFICIENT_KAMAS = 999999902 22 | PRICE_ERROR = 999999903 23 | 24 | def __init__(self, bid: MarketBid, new_price: int): 25 | super().__init__() 26 | self.bid = bid 27 | self.new_price = new_price 28 | self._received_sequence: Set[str] = set() 29 | self._logger = Logger() 30 | 31 | @property 32 | def bids_manager(self): 33 | return self._market_frame._bids_manager 34 | 35 | def run(self) -> bool: 36 | """Start the bid update process""" 37 | self._market_frame = Kernel().marketFrame 38 | if self._market_frame._market_type_open is None: 39 | return self.finish(1, "Market is not open") 40 | if self._market_frame._current_mode != "sell": 41 | return self.finish(1, "Market is not in sell mode") 42 | self.on(KernelEvent.MessageReceived, self._on_server_message) 43 | if self._can_afford_tax(self.new_price): 44 | self._logger.info(f"Updating bid {self.bid.uid} to price {self.new_price}") 45 | self._market_frame.send_update_listing(self.bid.uid, self.bid.quantity, self.new_price) 46 | else: 47 | self.finish(self.ERROR_CODES.INSUFFICIENT_KAMAS, "Insufficient kamas for tax") 48 | 49 | def _on_server_message(self, event, msg) -> None: 50 | """Watch for update sequence completion and provide detailed logging""" 51 | msg_type = msg.__class__.__name__ 52 | 53 | if msg_type in self.REQUIRED_SEQUENCE: 54 | self._received_sequence.add(msg_type) 55 | # remaining = self.REQUIRED_SEQUENCE - self._received_sequence 56 | # self._logger.debug( 57 | # f"Received {msg_type} ({len(self._received_sequence)}/{len(self.REQUIRED_SEQUENCE)})" 58 | # + (f", waiting for: {', '.join(remaining)}" if remaining else "") 59 | # ) 60 | 61 | if msg_type == "KamasUpdateMessage": 62 | self._logger.info(f"Kamas updated: {msg.kamasTotal:,}") 63 | elif msg_type == "ExchangeBidHouseItemRemoveOkMessage": 64 | self._logger.info("Old listing removed successfully") 65 | elif msg_type == "ExchangeBidHouseItemAddOkMessage": 66 | self._logger.info("New listing added successfully") 67 | 68 | if self._received_sequence >= self.REQUIRED_SEQUENCE: 69 | self._market_frame._state = "IDLE" 70 | self._logger.info("Update sequence completed successfully") 71 | self.finish(0) 72 | 73 | def _can_afford_tax(self, price: int) -> bool: 74 | """Check if player can afford tax""" 75 | tax = self.bids_manager.calculate_tax(price) 76 | return PlayedCharacterManager().characteristics.kamas >= tax 77 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bidhouse/GoToMarket.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.misc.Localizer import Localizer 2 | from pydofus2.com.ankamagames.dofus.datacenter.world.Area import Area 3 | from pydofus2.com.ankamagames.dofus.datacenter.world.Hint import Hint 4 | from pydofus2.com.ankamagames.dofus.datacenter.world.SubArea import SubArea 5 | from pydofus2.com.ankamagames.dofus.internalDatacenter.DataEnum import DataEnum 6 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 7 | from pydofus2.com.ankamagames.dofus.logic.common.managers.PlayerManager import PlayerManager 8 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 9 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 10 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 11 | 12 | 13 | class GoToMarket(AbstractBehavior): 14 | """Behavior for traveling to nearest marketplace of specified type""" 15 | 16 | ERROR_HDV_NOT_FOUND = 7676999 17 | 18 | def __init__(self, marketplace_gfx_id: int, exclude_market_at_maps: list[int] = None, item_level=200): 19 | """ 20 | Initialize market travel behavior 21 | Args: 22 | marketplace_gfx_id: GFX ID of target marketplace type 23 | """ 24 | super().__init__() 25 | self._logger = Logger() 26 | self.marketplace_gfx_id = marketplace_gfx_id 27 | self.hdv_vertex = None 28 | self.path_to_market = None 29 | if exclude_market_at_maps is None: 30 | exclude_market_at_maps = [] 31 | self.exclude_market_at_maps = exclude_market_at_maps 32 | self.item_level = item_level 33 | 34 | def run(self) -> bool: 35 | """Start travel to marketplace""" 36 | self._search_path_to_market() 37 | 38 | def _on_market_search_result(self, code, error, path): 39 | if path is None or error: 40 | return self.finish(self.ERROR_HDV_NOT_FOUND, f"No accessible marketplace found, search ended with result error[{code}] {error}") 41 | 42 | self.path_to_market = path 43 | 44 | if len(path) == 0: 45 | self.hdv_vertex = PlayedCharacterManager().currVertex 46 | 47 | else: 48 | self.hdv_vertex = path[-1].dst 49 | 50 | Logger().debug(f"Found market {len(path)} maps away at vertex {self.hdv_vertex}") 51 | 52 | if ( 53 | self.hdv_vertex != PlayedCharacterManager().currVertex 54 | or PlayedCharacterManager().currVertex.mapId in self.exclude_market_at_maps 55 | ): 56 | self.autoTrip( 57 | path=self.path_to_market, 58 | callback=self._on_market_map_reached, 59 | ) 60 | else: 61 | self._on_market_map_reached(None, None) 62 | 63 | def _search_path_to_market(self) -> bool: 64 | """ 65 | Validate marketplace accessibility and setup paths 66 | Returns: bool indicating if access is valid 67 | """ 68 | Logger().debug(f"Looking for market for item level {self.item_level}") 69 | current_map = PlayedCharacterManager().currentMap.mapId 70 | if not current_map: 71 | return self.finish(1, "Couldn't determine player current map!") 72 | 73 | if self.item_level > 60: 74 | if PlayerManager().isBasicAccount(): 75 | return self.finish( 76 | 1, 77 | "Basic accounts can't sell higher than lvl 60 items!" 78 | ) 79 | 80 | self.exclude_market_at_maps = list(map(int, self.exclude_market_at_maps)) 81 | 82 | for hint in Hint.getHints(): 83 | if int(hint.gfx) != self.marketplace_gfx_id: 84 | continue 85 | 86 | if int(hint.mapId) not in self.exclude_market_at_maps: 87 | map_sub_area = SubArea.getSubAreaByMapId(hint.mapId) 88 | if map_sub_area.areaId in [ 89 | DataEnum.ANKARNAM_AREA_ID, 90 | DataEnum.ASTRUB_AREA_ID, 91 | ]: # exclude ankarnam and astrub markets for items with level higher than 60 ! 92 | Logger().debug(f"Exclude market at mapId {hint.mapId} because it is in area {map_sub_area.area.name}") 93 | self.exclude_market_at_maps.append(int(hint.mapId)) 94 | 95 | Localizer.findClosestHintMapByGfxAsync( 96 | self.marketplace_gfx_id, 97 | callback=self._on_market_search_result, 98 | excludeMaps=self.exclude_market_at_maps 99 | ) 100 | 101 | def _on_market_map_reached(self, code: int, error: str) -> None: 102 | if not error: 103 | Kernel().marketFrame._market_mapId = PlayedCharacterManager().currVertex.mapId 104 | Kernel().marketFrame._market_gfx = self.marketplace_gfx_id 105 | self.finish(code, error) 106 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bidhouse/PlaceBid.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | from typing import Set 3 | 4 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 5 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 6 | from pydofus2.com.ankamagames.berilia.managers.KernelEventsManager import KernelEventsManager 7 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 8 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.InventoryManager import InventoryManager 9 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 10 | from pydofus2.com.ankamagames.dofus.network.messages.game.inventory.KamasUpdateMessage import KamasUpdateMessage 11 | from pydofus2.com.ankamagames.dofus.network.messages.game.inventory.exchanges.ExchangeBidHouseItemAddOkMessage import ExchangeBidHouseItemAddOkMessage 12 | from pydofus2.com.ankamagames.dofus.network.messages.game.inventory.items.ObjectDeletedMessage import ObjectDeletedMessage 13 | from pydofus2.com.ankamagames.dofus.network.messages.game.inventory.items.ObjectQuantityMessage import ObjectQuantityMessage 14 | from pydofus2.com.ankamagames.dofus.network.messages.game.inventory.items.InventoryWeightMessage import InventoryWeightMessage 15 | from pydofus2.com.ankamagames.jerakine.benchmark.BenchmarkTimer import BenchmarkTimer 16 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 17 | 18 | class PlaceBid(AbstractBehavior): 19 | """Handles the async logic for selling a single item in the marketplace""" 20 | 21 | class ERROR_CODES(Enum): 22 | INVALID_PRICE = 77777782 23 | NO_MORE_TO_SELL = auto() 24 | 25 | # Server response sequence for a sale 26 | REQUIRED_SEQUENCE = { 27 | "quantity", # Inventory quantity update 28 | "kamas", # Kamas update after tax 29 | "add", # Listing confirmation 30 | "weight" # Inventory weight update 31 | } 32 | 33 | def __init__(self, object_uid: int, quantity: int, price: int): 34 | super().__init__() 35 | self.object_uid = object_uid 36 | self.price = price 37 | self.quantity = quantity 38 | self._received_sequence: Set[str] = set() 39 | self._old_player_kamas = None 40 | 41 | def run(self) -> bool: 42 | """Start the sell operation""" 43 | if self.price <= 0: 44 | self.finish(self.ERROR_CODES.INVALID_PRICE, "Invalid price") 45 | return 46 | self.on(KernelEvent.ServerTextInfo, self._on_server_notif) 47 | self._old_player_kamas = PlayedCharacterManager().characteristics.kamas 48 | self.on(KernelEvent.MessageReceived, lambda _, m: self._process_message(m)) 49 | self.item = InventoryManager().inventory.getItem(self.object_uid) 50 | Kernel().marketFrame.create_listing(self.object_uid, self.quantity, self.price) 51 | 52 | def _on_server_notif(self, event, msgId, msgType, textId, msgContent, params): 53 | if textId == 5331: 54 | self.finish(self.ERROR_CODES.NO_MORE_TO_SELL, "Dont have enough quantity to sell") 55 | 56 | def _process_message(self, msg) -> None: 57 | """Track complete server response sequence""" 58 | if isinstance(msg, (ObjectQuantityMessage, ObjectDeletedMessage)): 59 | self._received_sequence.add("quantity") 60 | 61 | elif isinstance(msg, KamasUpdateMessage): 62 | self._received_sequence.add("kamas") 63 | amount_spent = self._old_player_kamas - msg.kamasTotal 64 | gid = self.item.item.objectGID 65 | KernelEventsManager().send(KernelEvent.KamasSpentOnSellTax, gid, self.quantity, amount_spent) 66 | 67 | elif isinstance(msg, ExchangeBidHouseItemAddOkMessage): 68 | self._received_sequence.add("add") 69 | 70 | elif isinstance(msg, InventoryWeightMessage): 71 | self._received_sequence.add("weight") 72 | 73 | # Check if sequence is complete 74 | if self._received_sequence >= self.REQUIRED_SEQUENCE: 75 | Kernel().marketFrame._state = "IDLE" 76 | Logger().info("Bid placed successfully") 77 | self.finish(0) -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bidhouse/UpdateMarketBids.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional 3 | 4 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 5 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 6 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 7 | 8 | class UpdateMarketBids(AbstractBehavior): 9 | """Updates multiple old listings to stay competitive""" 10 | 11 | MIN_UPDATE_AGE_HOURS = 3 12 | DEFAULT_MIN_PRICE_RATIO = 0.25 13 | 14 | class ERROR_CODES(Enum): 15 | OBJECT_NOT_FOUND = 999999901 16 | INVALID_ITEMS = 999999902 17 | PRICE_ERROR = 999999903 18 | MARKET_ERROR = 999999904 19 | 20 | def __init__(self, market_type, min_update_age_hours: float = MIN_UPDATE_AGE_HOURS, 21 | min_price_ratio: float = DEFAULT_MIN_PRICE_RATIO): 22 | super().__init__() 23 | self._logger = Logger() 24 | 25 | # Validate and filter items 26 | self.min_update_age_hours = min_update_age_hours 27 | self.min_price_ratio = min_price_ratio 28 | self._market_type = market_type 29 | self._market_frame = Kernel().marketFrame 30 | self._bids_manager = self._market_frame._bids_manager 31 | self._bids_to_update = None 32 | self._current_bid = None 33 | 34 | def run(self) -> bool: 35 | self.open_market(from_type=self._market_type, mode="sell", callback=self._on_market_open) 36 | 37 | def _on_market_open(self, code: int, error: Optional[str]) -> None: 38 | if error: 39 | return self.finish(code, f"Failed to open market: {error}") 40 | self._bids_to_update = self._bids_manager.get_all_updatable_bids(self.min_update_age_hours) 41 | self._check_bids_can_update() 42 | 43 | def _check_bids_can_update(self): 44 | if not self._bids_to_update: 45 | return self.close_market(lambda *_: self.finish(0)) 46 | self._current_bid = self._bids_to_update.pop(0) 47 | Kernel().marketFrame.check_price(self._current_bid.item_gid, lambda *_: self._on_price_infos()) 48 | 49 | def _on_price_infos(self): 50 | target_price, error = self._bids_manager.get_sell_price(self._current_bid.item_gid, self._current_bid.quantity, self.min_price_ratio) 51 | 52 | if error: 53 | self._logger.warning(f"Cannot get sell price for bid {self._current_bid.uid}: {error}") 54 | return self._check_bids_can_update() 55 | 56 | self.edit_bid_price(self._current_bid, target_price, self._on_update_complete) 57 | 58 | def _on_update_complete(self, code: int, error: Optional[str]) -> None: 59 | if error: 60 | self._logger.error( 61 | f"[{code}] Update failed for bid {self._current_bid.uid}: {error}" 62 | ) 63 | return self._check_bids_can_update() 64 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/bidhouse/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/bidhouse/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/chat/GetAllMapFightsDetails.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 2 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 3 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 4 | from pydofus2.com.ankamagames.dofus.kernel.net.ConnectionsHandler import ConnectionsHandler 5 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.roleplay.MapRunningFightDetailsMessage import MapRunningFightDetailsMessage 6 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.roleplay.MapRunningFightDetailsRequestMessage import MapRunningFightDetailsRequestMessage 7 | 8 | 9 | class GetMapFightDetails(AbstractBehavior): 10 | def __init__(self): 11 | super().__init__() 12 | 13 | def run(self): 14 | self.on(KernelEvent.MapFightDetails, self._on_fight_details) 15 | self.results = [] 16 | self._fights_processed = [] 17 | self.process_next() 18 | 19 | def process_next(self): 20 | for fightId, fight in Kernel().roleplayEntitiesFrame._fights.items(): 21 | if fightId not in self._fights_processed: 22 | self._fights_processed.append(fightId) 23 | msg = MapRunningFightDetailsRequestMessage() 24 | msg.init(fightId) 25 | ConnectionsHandler().send(msg) 26 | return 27 | self.finish(0, None, self.results) 28 | 29 | def _on_fight_details(self, event, msg: MapRunningFightDetailsMessage): 30 | self.results.append(msg) 31 | self._fights_processed.append(msg.fightId) 32 | self.process_next() 33 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/craft/CraftItem.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 2 | from pydofus2.com.ankamagames.berilia.managers.Listener import Listener 3 | 4 | 5 | class CraftItem(AbstractBehavior): 6 | 7 | def __init__(self): 8 | self.guestDisconnectedListener: Listener = None 9 | super().__init__() 10 | 11 | def run(self, bank, atelier, itemId) -> bool: 12 | self.atelier = atelier 13 | self.itemId = itemId 14 | self.bank = bank 15 | self.unLoad() 16 | 17 | def unLoad(self): 18 | def onBankUnload(code, err): 19 | if err: 20 | pass 21 | self.retrieveRecepie() 22 | 23 | def retrieveRecepie(self): 24 | def onretrieve(code, err): 25 | if err: 26 | pass 27 | self.goToAtelier() -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/craft/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/craft/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/exchange/CollectItems.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 4 | from pyd2bot.logic.roleplay.behaviors.bank.UnloadInBank import UnloadInBank 5 | from pyd2bot.logic.roleplay.behaviors.exchange.BotExchange import ( 6 | BotExchange, ExchangeDirectionEnum) 7 | from pyd2bot.misc.BotEventsManager import BotEventsManager 8 | from pyd2bot.misc.Localizer import BankInfos 9 | from pyd2bot.data.models import Character 10 | from pydofus2.com.ankamagames.berilia.managers.Listener import Listener 11 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 12 | 13 | 14 | class CollectState(Enum): 15 | WAITING_MAP = 0 16 | IDLE = 4 17 | GOING_TO_BANK = 1 18 | INSIDE_BANK = 8 19 | TREATING_BOT_UNLOAD = 2 20 | UNLOADING_IN_BANK = 3 21 | WAITING_FOR_BOT_TO_ARRIVE = 5 22 | EXCHANGING_WITH_GUEST = 6 23 | EXCHANGE_OPEN_REQUEST_RECEIVED = 7 24 | EXCHANGE_OPEN = 9 25 | EXCHANGE_ACCEPT_SENT = 10 26 | 27 | 28 | class CollectItems(AbstractBehavior): 29 | 30 | def __init__(self): 31 | self.guestDisconnectedListener: Listener = None 32 | super().__init__() 33 | 34 | def run(self, bankInfos: BankInfos, guest: Character, items: list = None) -> bool: 35 | Logger().info(f"[CollectFromGuest] collect from {guest.accountId} started") 36 | self.guest = guest 37 | self.bankInfos = bankInfos 38 | self.items = items 39 | self.state = CollectState.GOING_TO_BANK 40 | self.guestDisconnectedListener = BotEventsManager().onceBotDisconnected(self.guest.accountId, self.onGuestDisconnected, originator=self) 41 | self.autoTrip(self.bankInfos.npcMapId, callback=self.onTripEnded) 42 | 43 | def onGuestDisconnected(self): 44 | Logger().error("[CollectFromGuest] Guest disconnected!") 45 | if self.state == CollectState.EXCHANGING_WITH_GUEST: 46 | BotExchange().stop() 47 | self.finish(0) 48 | 49 | def onTripEnded(self, code, error): 50 | if not self.isRunning(): 51 | return 52 | if error is not None: 53 | return self.finish(False, error) 54 | self.state = CollectState.EXCHANGING_WITH_GUEST 55 | BotExchange().start(ExchangeDirectionEnum.RECEIVE, self.guest, self.items, callback=self.onExchangeConcluded, parent=self) 56 | 57 | def onExchangeConcluded(self, code, error): 58 | self.guestDisconnectedListener.delete() 59 | if error: 60 | if code == 516493: # Inventory full 61 | Logger().error(error) 62 | UnloadInBank().start(True, self.bankInfos, callback=self.finish, parent=self) 63 | self.state = CollectState.UNLOADING_IN_BANK 64 | return 65 | return self.finish(code, error) 66 | Logger().info("[CollectFromGuest] Exchange with guest ended successfully.") 67 | UnloadInBank().start(True, self.bankInfos, callback=self.finish, parent=self) 68 | self.state = CollectState.UNLOADING_IN_BANK 69 | 70 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/exchange/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/exchange/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/farm/CollectableResource.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import TYPE_CHECKING, List 3 | 4 | from pyd2bot.data.models import JobFilter 5 | from pydofus2.com.ankamagames.atouin.managers.MapDisplayManager import \ 6 | MapDisplayManager 7 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 8 | PlayedCharacterManager 9 | from pydofus2.com.ankamagames.dofus.logic.game.roleplay.frames.RoleplayInteractivesFrame import \ 10 | CollectableElement 11 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 12 | from pydofus2.com.ankamagames.jerakine.pathfinding.Pathfinding import \ 13 | PathFinding 14 | 15 | if TYPE_CHECKING: 16 | from pyd2bot.logic.roleplay.behaviors.farm.AbstractFarmBehavior import \ 17 | AbstractFarmBehavior 18 | 19 | class CollectableResource: 20 | def __init__(self, it: CollectableElement): 21 | self.resource = it 22 | self._nearestCell = None 23 | self.timeSinceLastTimeCollected = None 24 | 25 | @property 26 | def uid(self): 27 | return self.resource.id 28 | 29 | @property 30 | def resourceId(self): 31 | return self.resource.skill.gatheredRessource.id 32 | 33 | @property 34 | def jobId(self): 35 | return self.resource.skill.parentJobId 36 | 37 | @property 38 | def reachable(self): 39 | return self.nearestCell is not None and self.nearestCell.distanceTo(self.position) <= self.resource.skill.range 40 | 41 | @property 42 | def distance(self): 43 | movePath = PathFinding().findPath(PlayedCharacterManager().entity.position, self.position) 44 | if movePath is None: 45 | return -1 46 | return len(movePath.path) 47 | 48 | @property 49 | def nearestCell(self): 50 | if not self._nearestCell: 51 | playerEntity = PlayedCharacterManager().entity 52 | if playerEntity is None: 53 | Logger().debug("Player entity not found!") 54 | self._nearestCell = None 55 | return None 56 | movePath = PathFinding().findPath(playerEntity.position, self.position) 57 | if movePath is None: 58 | self._nearestCell = None 59 | return None 60 | self._nearestCell = movePath.end 61 | return self._nearestCell 62 | 63 | @property 64 | def position(self): 65 | return MapDisplayManager().getIdentifiedElementPosition(self.resource.id) 66 | 67 | @property 68 | def hasRequiredLevel(self): 69 | return PlayedCharacterManager().getJobLevel(self.resource.skill.parentJobId) >= self.resource.skill.levelMin 70 | 71 | def isFiltered(self, jobFilters: List[JobFilter]) -> bool: 72 | for jobFilter in jobFilters: 73 | if jobFilter.matchesResource(self.jobId, self.resourceId): 74 | return False 75 | return True 76 | 77 | @property 78 | def canCollect(self): 79 | return self.resource.enabled and self.hasRequiredLevel and self.reachable 80 | 81 | def canFarm(self, jobFilters: List[JobFilter]=None): 82 | if jobFilters: 83 | return self.canCollect and not self.isFiltered(jobFilters) 84 | return self.canCollect 85 | 86 | def farm(self, callback, caller: 'AbstractFarmBehavior'=None): 87 | caller.use_skill( 88 | elementId=self.resource.id, 89 | skilluid=self.resource.interactiveSkill.skillInstanceUid, 90 | cell=self.nearestCell.cellId, 91 | callback=callback, 92 | ) 93 | 94 | def __hash__(self) -> int: 95 | return self.resource.id 96 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/farm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/farm/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/fight/GroupLeaderFarmFights.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | from pyd2bot.logic.roleplay.behaviors.fight.SoloFarmFights import SoloFarmFights 3 | from pyd2bot.logic.roleplay.behaviors.party.WaitForMembersToShow import \ 4 | WaitForMembersToShow 5 | from pyd2bot.logic.roleplay.messages.FollowTransitionMessage import \ 6 | FollowTransitionMessage 7 | from pyd2bot.logic.roleplay.messages.MoveToVertexMessage import \ 8 | MoveToVertexMessage 9 | from pyd2bot.farmPaths.AbstractFarmPath import AbstractFarmPath 10 | from pyd2bot.data.models import Character 11 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 12 | from pydofus2.com.ankamagames.berilia.managers.KernelEventsManager import \ 13 | KernelEventsManager 14 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 15 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.TransitionTypeEnum import \ 16 | TransitionTypeEnum 17 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.Vertex import Vertex 18 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 19 | 20 | if TYPE_CHECKING: 21 | pass 22 | 23 | class GroupLeaderFarmFights(SoloFarmFights): 24 | 25 | def __init__(self, 26 | leader: Character, 27 | path: AbstractFarmPath, 28 | fightsPerMinute: int, 29 | fightPartyMembers: list[Character], 30 | monsterLvlCoefDiff=None, 31 | followers: list[Character]=None, 32 | timeout=None 33 | ): 34 | super().__init__(path, fightsPerMinute, fightPartyMembers, monsterLvlCoefDiff, timeout) 35 | self.leader = leader 36 | self.followers = followers 37 | 38 | def _move_to_next_step(self): 39 | super()._move_to_next_step() 40 | self.askFollowersMoveToVertex(self._currEdge.dst) 41 | 42 | def makeAction(self): 43 | if self.followers: 44 | Logger().info("Waiting for party members to be idle.") 45 | self.waitForMembersIdle(self.followers, self.leader, callback=self.onMembersIdle) 46 | return False 47 | super().makeAction() 48 | 49 | def onMembersIdle(self, code, err): 50 | if err: 51 | return KernelEventsManager().send(KernelEvent.ClientRestart, f"Wait members idle failed for reason : {err}") 52 | if not self.allMembersOnSameMap(): 53 | Logger().warning("Followers are not all on the same map!") 54 | self.askFollowersMoveToVertex(self.path.currentVertex) 55 | self.waitForMembersToShow(self.followers, callback=self.onMembersShowed) 56 | return False 57 | super().makeAction() 58 | 59 | def onMembersShowed(self, code, err): 60 | if err: 61 | if code == WaitForMembersToShow.MEMBER_DISCONNECTED: 62 | Logger().warning(f"Member {err} disconnected while waiting for them to show up") 63 | else: 64 | KernelEventsManager().send(KernelEvent.ClientRestart, f"Error while waiting for members to show up: {err}") 65 | return False 66 | Logger().warning("Followers are all on same map") 67 | self.makeAction() 68 | 69 | def askMembersFollow(self, transition: TransitionTypeEnum, dstMapId): 70 | for follower in self.followers: 71 | Kernel.getInstance(follower.accountId).worker.process(FollowTransitionMessage(transition, dstMapId)) 72 | 73 | def askFollowersMoveToVertex(self, vertex: Vertex): 74 | for follower in self.followers: 75 | entity = Kernel().roleplayEntitiesFrame.getEntityInfos(follower.id) 76 | if not entity: 77 | Kernel.getInstance(follower.accountId).worker.process(MoveToVertexMessage(vertex)) 78 | Logger().debug(f"Asked follower {follower.accountId} to go to farm start vertex") 79 | 80 | def allMembersOnSameMap(self): 81 | for follower in self.followers: 82 | if Kernel().roleplayEntitiesFrame is None: 83 | return False 84 | entity = Kernel().roleplayEntitiesFrame.getEntityInfos(follower.id) 85 | if not entity: 86 | return False 87 | return True 88 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/fight/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/fight/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/inventory/UseItem.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 2 | from pydofus2.com.ankamagames.atouin.HaapiEventsManager import HaapiEventsManager 3 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 4 | from pydofus2.com.ankamagames.dofus.internalDatacenter.items.ItemWrapper import \ 5 | ItemWrapper 6 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 7 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 8 | 9 | 10 | class UseItem(AbstractBehavior): 11 | 12 | def __init__(self, iw: ItemWrapper, qty: int) -> None: 13 | super().__init__() 14 | self.item = iw 15 | self.qty = qty 16 | 17 | def run(self): 18 | self.once(KernelEvent.ObjectDeleted, self._on_item_deleted, timeout=5, ontimeout=lambda *_: self.finish(2222, "Use item timed out!")) 19 | self.on(KernelEvent.ObjectAdded, self._on_item_added) 20 | HaapiEventsManager().sendInventoryOpenEvent() 21 | use_multiple = self.item.typeId != 172 # don't try to use multiple for chests 22 | Kernel().inventoryManagementFrame.useItem(self.item, self.qty, use_multiple=use_multiple) 23 | 24 | def _on_item_deleted(self, event, item_uid): 25 | if item_uid == self.item.objectUID: 26 | self.finish(0) 27 | else: 28 | Logger().warning(f"received object deleted event for object other than the one we are consuming!") 29 | 30 | def _on_item_added(self, event, item: ItemWrapper, quantity): 31 | if item.objectGID == self.item.objectGID and quantity < 0: 32 | if -quantity > self.qty: 33 | Logger().warning("Lost more than the quantity used!") 34 | return self.finish(0) 35 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/inventory/UseItemsByType.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 2 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.InventoryManager import InventoryManager 3 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 4 | 5 | 6 | class UseItemsByType(AbstractBehavior): 7 | 8 | def __init__(self, item_type: int) -> None: 9 | super().__init__() 10 | self.item_type = item_type 11 | self.item = None 12 | 13 | def run(self): 14 | self._process_next() 15 | 16 | @classmethod 17 | def has_items(cls, item_type): 18 | inventory_items = InventoryManager().inventory.getView("storageConsumables").content 19 | return [item for item in inventory_items if item.typeId == item_type] 20 | 21 | def _process_next(self): 22 | self._items_to_use = self.has_items(self.item_type) 23 | if not self._items_to_use: 24 | return self.finish(0) 25 | 26 | Logger().debug(f"Found {len(self._items_to_use)} items to use") 27 | self.item = self._items_to_use.pop() 28 | self.use_item(self.item, self.item.quantity, self._on_item_used) 29 | 30 | def _on_item_used(self, code, error): 31 | if error: 32 | Logger().error(f"Error using item {self.item.objectUID}: {error}") 33 | 34 | self._process_next() -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/inventory/UseTeleportItem.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 3 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 4 | from pydofus2.com.ankamagames.berilia.managers.Listener import Listener 5 | from pydofus2.com.ankamagames.dofus.internalDatacenter.items.ItemWrapper import \ 6 | ItemWrapper 7 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 8 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 9 | 10 | 11 | class UseTeleportItem(AbstractBehavior): 12 | MAX_TRIES = 3 13 | 14 | class errors(Enum): 15 | CANT_USE_ITEM_IN_MAP = auto() 16 | ALREADY_AT_DESTINATION = auto() 17 | TIME_OUT = auto() 18 | 19 | def __init__(self) -> None: 20 | self.nbr_tries = 0 21 | super().__init__() 22 | 23 | def run(self, iw: ItemWrapper): 24 | self.once_map_rendered( 25 | lambda *_: self.finish(0), 26 | timeout=20, 27 | ontimeout=self.onTimeout 28 | ) 29 | self.on(KernelEvent.ServerTextInfo, self.onServerInfo) 30 | if not Kernel().inventoryManagementFrame.useItem(iw): 31 | return self.finish(1, "Couldn't send use teleport item request") 32 | 33 | def onServerInfo(self, event, msgId, msgType, textId, msgContent, params): 34 | if textId == 4641: 35 | self.finish(self.errors.CANT_USE_ITEM_IN_MAP, f"Cant use this teleport item on this map") 36 | elif textId == 781094: 37 | self.finish(self.errors.ALREADY_AT_DESTINATION, f"Already at destination map!") 38 | 39 | def onTimeout(self, listener: Listener): 40 | Logger().error("Use item timed out!") 41 | if self.nbr_tries < self.MAX_TRIES: 42 | listener.armTimer() 43 | self.nbr_tries += 1 44 | else: 45 | self.finish(self.errors.TIME_OUT, "Use teleport item timedout!") 46 | 47 | 48 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/inventory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/inventory/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/misc/Resurrect.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 2 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 3 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 4 | from pydofus2.com.ankamagames.dofus.kernel.net.ConnectionsHandler import \ 5 | ConnectionsHandler 6 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 7 | PlayedCharacterManager 8 | from pydofus2.com.ankamagames.dofus.network.enums.PlayerLifeStatusEnum import \ 9 | PlayerLifeStatusEnum 10 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.roleplay.death.GameRolePlayFreeSoulRequestMessage import \ 11 | GameRolePlayFreeSoulRequestMessage 12 | from pydofus2.com.ankamagames.jerakine.benchmark.BenchmarkTimer import \ 13 | BenchmarkTimer 14 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 15 | 16 | 17 | class Resurrect(AbstractBehavior): 18 | CEMETERY_MAP_LOADED_TIMEOUT = 7 19 | 20 | def __init__(self) -> None: 21 | super().__init__() 22 | self.requestTimer = None 23 | self.cemeteryMapLoadedListener = None 24 | 25 | def run(self) -> bool: 26 | self.phenixMapId = Kernel().playedCharacterUpdatesFrame._phenixMapId 27 | if not PlayedCharacterManager().currentMap: 28 | return self.once_map_rendered(self.start) 29 | self.on(KernelEvent.PlayerStateChanged, self.onPlayerStateChange) 30 | if PlayedCharacterManager().player_life_status == PlayerLifeStatusEnum.STATUS_PHANTOM: 31 | Logger().debug(f"Traveling to phenix map {self.phenixMapId}") 32 | self.autoTrip(dstMapId=self.phenixMapId, callback=self.onPhenixMapReached) 33 | elif PlayedCharacterManager().player_life_status == PlayerLifeStatusEnum.STATUS_TOMBSTONE: 34 | self.cemeteryMapLoadedListener = self.once_map_rendered(self.onCemeteryMapLoaded) 35 | self.releaseSoulRequest() 36 | else: 37 | self.finish(1, f"Unknown user state {PlayedCharacterManager().player_life_status}!!") 38 | 39 | def onPlayerStateChange(self, event, playerState: PlayerLifeStatusEnum, phenixMapId): 40 | self.phenixMapId = phenixMapId 41 | Logger().debug(f"Phoenix mapId : {phenixMapId}") 42 | if playerState == PlayerLifeStatusEnum.STATUS_PHANTOM: 43 | Logger().info(f"Player soul released waiting for cemetery map to load.") 44 | elif playerState == PlayerLifeStatusEnum.STATUS_ALIVE: 45 | Logger().info("Player is alive and kicking.") 46 | self.finish(0) 47 | 48 | def onCemeteryMapLoaded(self, event_id=None): 49 | Logger().debug(f"Cemetery map loaded.") 50 | if self.requestTimer: 51 | self.requestTimer.cancel() 52 | 53 | self.autoTrip(dstMapId=self.phenixMapId, callback=self.onPhenixMapReached) 54 | 55 | def onPhenixSkillUsed(self, code, error, iePosition=None): 56 | if error: 57 | return self.finish(code, error) 58 | 59 | Logger().info("Phenix revive skill used") 60 | 61 | def onPhenixMapReached(self, code, error): 62 | if error: 63 | return self.finish(code, error) 64 | 65 | if Kernel().interactiveFrame: 66 | reviveIE = Kernel().interactiveFrame.getReviveIe() 67 | self.use_skill(ie=reviveIE, callback=self.onPhenixSkillUsed) 68 | else: 69 | self.once_frame_pushed("RoleplayInteractivesFrame", self.onPhenixMapReached) 70 | 71 | def onCemeteryMapLoadedTimeout(self): 72 | Logger().error("Cemetery map loaded timeout.") 73 | self.cemeteryMapLoadedListener.delete() 74 | self.autoTrip(dstMapId=self.phenixMapId, callback=self.onPhenixMapReached) 75 | 76 | def releaseSoulRequest(self): 77 | self.requestTimer = BenchmarkTimer(self.CEMETERY_MAP_LOADED_TIMEOUT, self.onCemeteryMapLoadedTimeout) 78 | self.requestTimer.start() 79 | ConnectionsHandler().send(GameRolePlayFreeSoulRequestMessage()) 80 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/misc/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/mount/PutPetsMount.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.data.enums import ServerNotificationEnum 2 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 3 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 4 | from pydofus2.com.ankamagames.dofus.internalDatacenter.DataEnum import DataEnum 5 | from pydofus2.com.ankamagames.dofus.internalDatacenter.items.ItemWrapper import ItemWrapper 6 | from pydofus2.com.ankamagames.dofus.kernel.net.ConnectionsHandler import ConnectionsHandler 7 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.InactivityManager import InactivityManager 8 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.InventoryManager import InventoryManager 9 | from pydofus2.com.ankamagames.dofus.network.enums.CharacterInventoryPositionEnum import CharacterInventoryPositionEnum 10 | from pydofus2.com.ankamagames.dofus.network.messages.game.inventory.items.ObjectSetPositionMessage import ObjectSetPositionMessage 11 | from pydofus2.com.ankamagames.jerakine.benchmark.BenchmarkTimer import BenchmarkTimer 12 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 13 | 14 | 15 | class PutPetsMount(AbstractBehavior): 16 | 17 | def __init__(self) -> None: 18 | super().__init__() 19 | 20 | def run(self): 21 | pet_mounts_available_in_inventory = self.has_items() 22 | if not pet_mounts_available_in_inventory: 23 | Logger().debug('Has no pet mounts in inventory') 24 | return self.finish(0) 25 | self.on(KernelEvent.ServerTextInfo, self.onServerTextInfo) 26 | self.sendPetsMountPut(pet_mounts_available_in_inventory[0]) 27 | BenchmarkTimer(2, lambda: self.finish(0)).start() 28 | 29 | def onServerTextInfo(self, event, msgId, msgType, textId, text, params): 30 | if textId == ServerNotificationEnum.STATUS_DOES_NOT_ALLOW_ACTION: # Mount has no energy left 31 | self.finish(textId, text) 32 | 33 | @classmethod 34 | def has_items(cls): 35 | inventory_items = InventoryManager().inventory.getView("storageEquipment").content 36 | return [item for item in inventory_items if item is not None and item.typeId == DataEnum.ITEM_TYPE_PETSMOUNT] 37 | 38 | def sendPetsMountPut(self, mount_iw: ItemWrapper): 39 | msg = ObjectSetPositionMessage() 40 | msg.init(mount_iw.objectUID, CharacterInventoryPositionEnum.ACCESSORY_POSITION_PETS, 1) 41 | ConnectionsHandler().send(msg) 42 | InactivityManager().activity() 43 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/mount/ToggleRideMount.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 4 | from pyd2bot.data.enums import ServerNotificationEnum 5 | from pydofus2.com.ankamagames.atouin.HaapiEventsManager import HaapiEventsManager 6 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 7 | from pydofus2.com.ankamagames.berilia.managers.KernelEventsManager import KernelEventsManager 8 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 9 | from pydofus2.com.ankamagames.dofus.logic.common.managers.PlayerManager import PlayerManager 10 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 11 | PlayedCharacterManager 12 | from pydofus2.com.ankamagames.dofus.uiApi.PlayedCharacterApi import PlayedCharacterApi 13 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 14 | 15 | if TYPE_CHECKING: 16 | pass 17 | 18 | 19 | class ToggleRideMount(AbstractBehavior): 20 | NEED_LVL_60 = 77887661 21 | NO_ENERGY_LEFT = 77887662 22 | NO_EQUIPPED_MOUNT = 77887663 23 | ALREADY_RIDING = 77887664 24 | ONLY_SUBSCRIBED = 77887665 25 | DIDNT_GET_WANTED_STATE = 77887666 26 | 27 | def __init__(self) -> None: 28 | super().__init__() 29 | 30 | def onServerTextInfo(self, event, msgId, msgType, textId, text, params): 31 | if textId == ServerNotificationEnum.MOUNT_HAS_NO_ENERGY_LEFT: # Mount has no energy left 32 | self.finish(self.NO_ENERGY_LEFT, text) 33 | 34 | def useToggleRideMountShortcut(self): 35 | if not Kernel().mountFrame: 36 | return self.once_frame_pushed('MountFrame', self.useToggleRideMountShortcut) 37 | KernelEventsManager().once(KernelEvent.MountRiding, self.onMountRiding) 38 | HaapiEventsManager().registerShortcutUse("openMount") 39 | if not Kernel().worker.terminated.wait(0.3): 40 | Kernel().mountFrame.mountToggleRidingRequest() 41 | 42 | def onMountRiding(self, event, isRiding): 43 | if self.wanted_ride_state is not None and self.wanted_ride_state != isRiding: 44 | return self.finish(self.DIDNT_GET_WANTED_STATE, "Mount riding state is not as wanted") 45 | self.finish(0) 46 | 47 | def run(self, wanted_ride_state=None) -> bool: 48 | self.wanted_ride_state = wanted_ride_state 49 | self.mount = PlayedCharacterApi.getMount() 50 | if not self.mount: 51 | return self.finish(self.NO_EQUIPPED_MOUNT, "No mount") 52 | if PlayerManager().isBasicAccount(): 53 | return self.finish(self.ONLY_SUBSCRIBED, "Only subscribed accounts can ride mounts") 54 | if PlayedCharacterManager().infos.level < 60: 55 | return self.finish(self.NEED_LVL_60, "Need to be level 10 to ride a mount") 56 | energy_left_ration = self.mount.energy / self.mount.energyMax 57 | if energy_left_ration < 0.05: 58 | return self.finish(self.NO_ENERGY_LEFT, "Mount has no energy left to ride") 59 | self.on(KernelEvent.ServerTextInfo, self.onServerTextInfo) 60 | if wanted_ride_state is not None: 61 | if wanted_ride_state and PlayedCharacterApi.isRiding(): 62 | return self.finish(self.ALREADY_RIDING, "Already riding mount") 63 | if not wanted_ride_state and not PlayedCharacterApi.isRiding(): 64 | return self.finish(self.ALREADY_RIDING, "Not riding mount can't dismount") 65 | self.useToggleRideMountShortcut() -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/mount/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/mount/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/movement/AstarPathFinder.py: -------------------------------------------------------------------------------- 1 | from time import perf_counter 2 | from typing import Optional 3 | 4 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 5 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 6 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.astar.AStar import AStar 7 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.WorldGraph import WorldGraph 8 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 9 | 10 | 11 | class AstarPathFinder(AbstractBehavior): 12 | NO_PATH_FOUND = 2202203 13 | 14 | def __init__(self, dst_map_id: int, linked_zone: Optional[int]): 15 | super().__init__() 16 | self._start_time: float = 0 17 | self.dst_map_id = dst_map_id 18 | self.linked_zone = linked_zone 19 | 20 | def run(self, ) -> None: 21 | src = PlayedCharacterManager().currVertex 22 | if src is None: 23 | Logger().warning("Called path finder before player was added to the scene!") 24 | return self.once_map_rendered(self.run) 25 | 26 | if self.linked_zone is None and PlayedCharacterManager().currentMap.mapId == self.dst_map_id: 27 | return self.finish(0, None, []) 28 | 29 | if self.linked_zone is None: 30 | vertices = list(WorldGraph().getVertices(self.dst_map_id).values()) 31 | else: 32 | vertices = WorldGraph().getVertex(self.dst_map_id, self.linked_zone) 33 | 34 | if not vertices: 35 | return self.finish(self.NO_PATH_FOUND, "No valid destination vertices found", None) 36 | 37 | self.vertices = vertices 38 | self._start_time = perf_counter() 39 | src = PlayedCharacterManager().currVertex 40 | AStar().search_async(src, self.vertices, callback=self._on_path_found_callback) 41 | 42 | def _on_path_found_callback(self, code, exc, path): 43 | search_time = perf_counter() - self._start_time 44 | Logger().info(f"Result found in {search_time}s") 45 | if exc is not None: 46 | raise exc 47 | 48 | if code == 0: 49 | self.finish(0, None, path) 50 | else: 51 | self.finish(self.NO_PATH_FOUND, "Unable to find path to dest map", None) 52 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/movement/GetOutOfAnkarnam.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 4 | from pydofus2.com.ankamagames.berilia.managers.Listener import Listener 5 | from pydofus2.com.ankamagames.dofus.datacenter.world.SubArea import SubArea 6 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 7 | PlayedCharacterManager 8 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 9 | 10 | if TYPE_CHECKING: 11 | pass 12 | 13 | 14 | class GetOutOfAnkarnam(AbstractBehavior): 15 | ASTRUB_MAPLOAD_TIMEOUT = 20203 16 | npcId = -20001 17 | npcMapId = 153880835 18 | openGoToAstrubActionId = 3 19 | hesitateReplayId = 36979 20 | goToAstrubReplyId = 36977 21 | ankarnamAreaId = 45 22 | astrubLandingMapId = 192416776 23 | 24 | def __init__(self) -> None: 25 | super().__init__() 26 | 27 | def onAstrubMapProcessed(self, event=None): 28 | self.finish(0, None) 29 | 30 | def onAstrubMapLoadTimeout(self, listener: Listener): 31 | return self.finish(self.ASTRUB_MAPLOAD_TIMEOUT, f"Load Astrub map '{self.astrubLandingMapId}' timedout!") 32 | 33 | def onGetOutOfIncarnamNpcEnd(self, code, error): 34 | if error: 35 | return self.finish(code, error) 36 | self.once_map_rendered( 37 | callback=self.onAstrubMapProcessed, 38 | mapId=self.astrubLandingMapId, 39 | timeout=20, 40 | ontimeout=self.onAstrubMapLoadTimeout 41 | ) 42 | 43 | def run(self) -> bool: 44 | sa = SubArea.getSubAreaByMapId(PlayedCharacterManager().currentMap.mapId) 45 | areaId = sa._area.id 46 | if areaId != self.ankarnamAreaId: 47 | Logger().debug(f"GetOutOfAnkarnam: Current area is not Ankarnam! (areaId: {areaId})") 48 | return self.finish(0) 49 | self.npc_dialog( 50 | self.npcMapId, 51 | self.npcId, 52 | self.openGoToAstrubActionId, 53 | { 54 | 30637: self.hesitateReplayId, 55 | 30638: self.goToAstrubReplyId, 56 | }, 57 | callback=self.onGetOutOfIncarnamNpcEnd, 58 | ) 59 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/movement/InteractiveMapChange.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional 3 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 4 | from pyd2bot.logic.roleplay.behaviors.skill.UseSkill import UseSkill 5 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 6 | from pydofus2.com.ankamagames.dofus.datacenter.interactives.Interactive import Interactive 7 | from pydofus2.com.ankamagames.dofus.internalDatacenter.DataEnum import DataEnum 8 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 9 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 10 | from pydofus2.com.ankamagames.dofus.logic.game.roleplay.frames.InteractiveElementData import InteractiveElementData 11 | from pydofus2.com.ankamagames.dofus.logic.game.roleplay.types.MovementFailError import MovementFailError 12 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 13 | 14 | class InteractiveMapChange(AbstractBehavior): 15 | class errors(Enum): 16 | LANDED_ON_WRONG_MAP = 1002 17 | IE_NOT_FOUND = 1003 18 | WRONG_IE_TYPE = 1004 19 | 20 | def __init__(self, dst_map_id: int, ie_elem_id: int, skill_id: int, cell_id: int) -> None: 21 | super().__init__() 22 | self.dst_map_id = dst_map_id 23 | self.ie_elem_id = ie_elem_id 24 | self.skill_id = skill_id 25 | self.cell_id = cell_id 26 | self.map_change_ie: Optional[InteractiveElementData] = None 27 | 28 | def run(self) -> None: 29 | # Check and validate the interactive element first 30 | self.map_change_ie = Kernel().interactiveFrame._ie.get(self.ie_elem_id) 31 | 32 | if not self.map_change_ie: 33 | return self.finish( 34 | self.errors.IE_NOT_FOUND, 35 | f"Unable to find interactive element {self.ie_elem_id}! Available: {Kernel().interactiveFrame._ie}" 36 | ) 37 | 38 | # Log IE info if available 39 | ie_type = Interactive.getInteractiveById(self.map_change_ie.element.elementTypeId) 40 | if ie_type: 41 | Logger().debug(f"Transition IE is {ie_type.name} ==> {ie_type.actionId}") 42 | 43 | # Check if it's a zaap - if so, we shouldn't handle it 44 | if self.map_change_ie.element.elementTypeId == DataEnum.ZAAP_TYPEID: 45 | return self.useZaap(self.dst_map_id, callback=self.finish) 46 | 47 | self.on( 48 | KernelEvent.CurrentMap, 49 | self.on_current_map, 50 | timeout=20, 51 | ontimeout=self.on_request_timeout 52 | ) 53 | 54 | self.on( 55 | KernelEvent.InteractiveUseError, 56 | self.on_use_error 57 | ) 58 | 59 | self.use_skill( 60 | elementId=self.ie_elem_id, 61 | skilluid=self.skill_id, 62 | cell=self.cell_id, 63 | exactDestination=False, 64 | callback=self.on_skill_used 65 | ) 66 | 67 | def on_skill_used(self, code: int, error: Optional[str]) -> None: 68 | if PlayedCharacterManager().isInFight: 69 | return self.stop() 70 | 71 | if error: 72 | return self.finish(code, error) 73 | 74 | Logger().debug("Map change IE used") 75 | 76 | def on_use_error(self, event) -> None: 77 | self.finish( 78 | MovementFailError.INTERACTIVE_USE_ERROR, 79 | "Failed to use interactive element" 80 | ) 81 | 82 | def on_current_map(self, event, map_id: int) -> None: 83 | self.clearListeners() 84 | if map_id == self.dst_map_id: 85 | callback = lambda: self.finish(0) 86 | else: 87 | callback = lambda: self.finish( 88 | self.errors.LANDED_ON_WRONG_MAP, 89 | f"Landed on new map '{map_id}', different from dest '{self.dst_map_id}'." 90 | ) 91 | self.once_map_rendered(callback=callback, mapId=map_id, timeout=20, ontimeout=self.on_dest_map_rendered_timeout) 92 | 93 | def on_dest_map_rendered_timeout(self, listener): 94 | if PlayedCharacterManager().isInFight: 95 | self.stop(True) 96 | return 97 | 98 | self.finish(222, "Request Map data timeout") 99 | 100 | def on_request_timeout(self, listener) -> None: 101 | if UseSkill().isRunning(): 102 | listener.armTimer() 103 | return 104 | 105 | self.finish( 106 | MovementFailError.MAP_CHANGE_TIMEOUT, 107 | "Map change request timed out" 108 | ) 109 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/movement/RequestMapData.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 2 | from pydofus2.com.ankamagames.atouin.managers.MapDisplayManager import \ 3 | MapDisplayManager 4 | from pydofus2.com.ankamagames.berilia.managers.Listener import Listener 5 | from pydofus2.com.ankamagames.dofus.kernel.net.ConnectionsHandler import \ 6 | ConnectionsHandler 7 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 8 | from pydofus2.com.ankamagames.dofus.network.messages.common.basic.BasicPingMessage import \ 9 | BasicPingMessage 10 | from pydofus2.com.ankamagames.dofus.network.messages.game.context.roleplay.MapInformationsRequestMessage import \ 11 | MapInformationsRequestMessage 12 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 13 | 14 | 15 | class RequestMapData(AbstractBehavior): 16 | REQUEST_MAPDATA_TIMEOUT = 5 17 | TIMEOUT_MAX_COUNT = 3 18 | TIMEOUT = 309 19 | CURRENT_MAP_NOT_FOUND = 304 20 | 21 | def __init__(self) -> None: 22 | self.mapDataRequestNbrFail = 0 23 | self.listener = None 24 | super().__init__() 25 | 26 | def run(self, mapId=None) -> bool: 27 | if not MapDisplayManager().currentMapPoint: 28 | return self.finish(self.CURRENT_MAP_NOT_FOUND, "Current Map point is None!") 29 | if not mapId: 30 | mapId = MapDisplayManager().currentMapPoint.mapId 31 | mapId = float(mapId) 32 | self.mapId = mapId 33 | # Logger().info(f"Requesting data for map {mapId}") 34 | self.listener = self.once_map_rendered( 35 | lambda: self.finish(0), 36 | mapId=mapId, 37 | timeout=self.REQUEST_MAPDATA_TIMEOUT, 38 | ontimeout=self.onMapDataRequestTimeout, 39 | ) 40 | self.sendRequest() 41 | 42 | def onMapDataRequestTimeout(self, listener: Listener): 43 | if PlayedCharacterManager().isFighting: 44 | return 45 | Logger().warning("Map data request timeout") 46 | pingMsg = BasicPingMessage() 47 | pingMsg.init(True) 48 | ConnectionsHandler().send(pingMsg) 49 | self.mapDataRequestNbrFail += 1 50 | if self.mapDataRequestNbrFail > self.TIMEOUT_MAX_COUNT: 51 | listener.delete() 52 | return self.finish(self.TIMEOUT, "Map data request timeout") 53 | listener.armTimer() 54 | self.sendRequest() 55 | 56 | def sendRequest(self) -> None: 57 | mirmsg = MapInformationsRequestMessage() 58 | mirmsg.init(self.mapId) 59 | ConnectionsHandler().send(mirmsg) -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/movement/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/movement/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/npc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/npc/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/party/WaitForMembersIdle.py: -------------------------------------------------------------------------------- 1 | import json 2 | import threading 3 | 4 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 5 | from pyd2bot.data.models import Character 6 | from pydofus2.com.ankamagames.atouin.managers.MapDisplayManager import \ 7 | MapDisplayManager 8 | from pydofus2.com.ankamagames.berilia.managers.Listener import Listener 9 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 10 | from pydofus2.com.ankamagames.dofus.kernel.net.ConnectionsHandler import \ 11 | ConnectionsHandler 12 | from pydofus2.com.ankamagames.dofus.kernel.net.ConnectionType import \ 13 | ConnectionType 14 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 15 | PlayedCharacterManager 16 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 17 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 18 | from pydofus2.com.ankamagames.berilia.managers.KernelEventsManager import \ 19 | KernelEventsManager 20 | 21 | class WaitForMembersIdle(AbstractBehavior): 22 | GET_STATUS_TIMEOUT = 992 23 | MEMBER_RECONNECT_WAIT_TIMEOUT = 30 24 | MEMBER_DISCONNECTED = 997 25 | 26 | def __init__(self) -> None: 27 | super().__init__() 28 | self.actorShowedListener: 'Listener' = None 29 | self.partyMemberLeftPartyListener = None 30 | self.memberStatus = dict[str, str]() 31 | self.members = list[Character]() 32 | 33 | def run(self, members: list[Character], leader: Character) -> bool: 34 | self.leader = leader 35 | self.members = members 36 | thread_name = threading.current_thread().name 37 | self.status_thread = threading.Thread(target=self.fetchStatuses, name=thread_name) 38 | self.status_thread.daemon = True 39 | self.status_thread.start() 40 | 41 | def fetchStatuses(self): 42 | Logger().debug("Fetch status started") 43 | if not self.isRunning(): 44 | return 45 | while not Kernel().worker.terminated.is_set(): 46 | Logger().debug("Fetching members statuses ...") 47 | self.memberStatus = {member.accountId: self.getMuleStatus(member.accountId) for member in self.members} 48 | Logger().debug(json.dumps(self.memberStatus, indent=2)) 49 | if any(status != "idle" for status in self.memberStatus.values()): 50 | Logger().info(f"Some of the members are not idle!") 51 | if any(status == "disconnected" for status in self.memberStatus.values()): 52 | Logger().debug("Some members are disconnected will wait for 10 seconds before fetching again") 53 | if Kernel().worker.terminated.wait(5): 54 | Logger().debug("fetch members status will shutdown because worker terminated!") 55 | return 56 | else: 57 | if Kernel().worker.terminated.wait(2): 58 | Logger().debug("fetch members status will shutdown because worker terminated!") 59 | return 60 | else: 61 | Logger().info(f"All members are idle.") 62 | return self.finish(0) 63 | Logger().debug("Fetch status finished because worker is terminated") 64 | 65 | def getMuleStatus(self, instanceId): 66 | Logger().debug(f"Fetching player {instanceId} status") 67 | try: 68 | if not ConnectionsHandler.getInstance(instanceId) or \ 69 | ConnectionsHandler.getInstance(instanceId).connectionType == ConnectionType.DISCONNECTED: 70 | return "disconnected" 71 | elif ConnectionsHandler.getInstance(instanceId).connectionType == ConnectionType.TO_LOGIN_SERVER: 72 | return "authenticating" 73 | if PlayedCharacterManager.getInstance(instanceId).isInFight: 74 | return "fighting" 75 | elif not Kernel.getInstance(instanceId).roleplayEntitiesFrame: 76 | return "outOfRolePlay" 77 | elif MapDisplayManager.getInstance(instanceId).currentDataMap is None: 78 | return "loadingMap" 79 | elif not Kernel.getInstance(instanceId).roleplayEntitiesFrame.mcidm_processed: 80 | return "processingMapData" 81 | elif PlayedCharacterManager.getInstance(instanceId).is_dead(): 82 | return "muleIsDead" 83 | Logger().debug(f"Checking if player {instanceId} is running behaviors") 84 | for behavior in AbstractBehavior.getSubs(instanceId): 85 | if type(behavior).__name__ != "MuleFighter" and behavior.isRunning() and not behavior.IS_BACKGROUND_TASK: 86 | return type(behavior).__name__ 87 | return "idle" 88 | except Exception as e: 89 | Logger().error("Something went wrong while fetching mule status", exc_info=True) 90 | KernelEventsManager().send(KernelEvent.ClientShutdown, f"Error while fetching mule status: {e}") 91 | raise 92 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/party/WaitForMembersToShow.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 2 | from pyd2bot.misc.BotEventsManager import BotEventsManager 3 | from pyd2bot.data.models import Character 4 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 5 | from pydofus2.com.ankamagames.berilia.managers.KernelEventsManager import \ 6 | KernelEventsManager 7 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 8 | from pydofus2.com.ankamagames.dofus.network.types.game.context.roleplay.GameRolePlayHumanoidInformations import \ 9 | GameRolePlayHumanoidInformations 10 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 11 | 12 | 13 | class WaitForMembersToShow(AbstractBehavior): 14 | MEMBER_LEFT_PARTY = 991 15 | MEMBER_DISCONNECTED = 997 16 | 17 | def __init__(self) -> None: 18 | super().__init__() 19 | 20 | def run(self, members: list[Character]) -> bool: 21 | self.members = members 22 | Logger().debug("Waiting for members to show up.") 23 | BotEventsManager().on(BotEventsManager.PLAYER_DISCONNECTED, self.onMemberDisconnected, originator=self) 24 | KernelEventsManager().on(KernelEvent.ActorShowed, self.onActorShowed, originator=self) 25 | 26 | def onTeamMemberShowed(self): 27 | if Kernel().roleplayEntitiesFrame is None: 28 | return KernelEventsManager().onceFramePushed("RoleplayEntitiesFrame", self.onTeamMemberShowed, originator=self) 29 | notShowed = [member.name for member in self.members if not Kernel().roleplayEntitiesFrame.getEntityInfos(member.id)] 30 | if len(notShowed) > 0: 31 | return Logger().info(f"Waiting for members {notShowed} to show up.") 32 | Logger().info("All party members showed.") 33 | self.finish(0) 34 | 35 | def onActorShowed(self, event, infos: "GameRolePlayHumanoidInformations"): 36 | Logger().info(f"Actor {infos.name} showed.") 37 | for member in self.members: 38 | if member.id == infos.contextualId: 39 | self.onTeamMemberShowed() 40 | 41 | def onMemberDisconnected(self, event, accountId, connectionType): 42 | for member in self.members: 43 | if member.accountId == accountId: 44 | Logger().warning(f"Member {accountId} disconnected while waiting for it to show up") 45 | return self.finish(self.MEMBER_DISCONNECTED, accountId) -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/party/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/party/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/quest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/quest/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/quest/treasure_hunt/FindHintNpc.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 2 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 3 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 4 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 5 | PlayedCharacterManager 6 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.WorldGraph import \ 7 | WorldGraph 8 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 9 | 10 | 11 | class FindHintNpc(AbstractBehavior): 12 | UNABLE_TO_FIND_HINT = 475556 13 | 14 | def __init__(self) -> None: 15 | super().__init__() 16 | self.npcId = None 17 | self.direction = None 18 | 19 | def run(self, npcId, direction): 20 | self.npcId = npcId 21 | self.direction = direction 22 | self.on(KernelEvent.TreasureHintInformation, self.onTreasureHintInfos) 23 | self._on_trip_finished(True, None) 24 | 25 | def onTreasureHintInfos(self, event, npcId): 26 | if npcId == self.npcId: 27 | Logger().debug(f"Hint npc found!!") 28 | return self.finish(0) 29 | 30 | def _on_trip_finished(self, code, err, transition=None): 31 | if err: 32 | return self.finish(code, err) 33 | 34 | found_thnpc = Kernel().roleplayEntitiesFrame.treasureHuntNpc 35 | if found_thnpc and found_thnpc.npcId == self.npcId: 36 | # while parsing new map data if the special treasure hunt npc is found it is stored in the treasureHuntNpc attribute 37 | # if it is found we simply return without ending the behavior because the entities frame it self will send the event treasureHuntNpc that will 38 | # be caught in this same behavior. 39 | # but if it is not found then we have to move to another map 40 | return 41 | 42 | map_id = WorldGraph().nextMapInDirection(PlayedCharacterManager().currVertex.mapId, self.direction) 43 | if not map_id: 44 | return self.finish(self.UNABLE_TO_FIND_HINT, f"Couldn't find NPC hint in the given direction") 45 | 46 | self.autoTrip( 47 | map_id, 48 | callback=self._on_trip_finished 49 | ) 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/quest/treasure_hunt/TakeTreasureHuntQuest.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 3 | from pyd2bot.logic.roleplay.behaviors.inventory.UseTeleportItem import UseTeleportItem 4 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 5 | from pydofus2.com.ankamagames.dofus.internalDatacenter.DataEnum import DataEnum 6 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 7 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 8 | from pydofus2.com.ankamagames.dofus.network.enums.TreasureHuntRequestEnum import TreasureHuntRequestEnum 9 | from pydofus2.com.ankamagames.dofus.network.enums.TreasureHuntTypeEnum import TreasureHuntTypeEnum 10 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 11 | from pydofus2.mapTools import MapTools 12 | 13 | class TakeTreasureHuntQuest(AbstractBehavior): 14 | """Behavior for taking a treasure hunt quest from the ATM.""" 15 | 16 | # Constants from original code 17 | TAKE_QUEST_MAPID = 128452097 18 | TAKE_QUEST_ZONE_ID = 1 19 | TREASURE_HUNT_ATM_IE_ID = 484993 20 | TREASURE_HUNT_ATM_SKILLUID = 152643320 21 | ZAAP_HUNT_MAP = 142087694 22 | FARM_RESOURCES = False 23 | 24 | 25 | class errors(Enum): 26 | UNABLE_TO_TAKE_QUEST = auto() 27 | UNSUPPORTED_HUNT_TYPE = auto() 28 | 29 | def __init__(self): 30 | super().__init__() 31 | self.maxCost = 2000 # Default max cost for zaap travel 32 | 33 | def run(self): 34 | """Start the quest-taking process.""" 35 | Logger().debug("Starting process to take treasure hunt quest") 36 | self.goToHuntAtm() 37 | 38 | def goToHuntAtm(self): 39 | """Navigate to the treasure hunt ATM.""" 40 | Logger().debug("AutoTraveling to treasure hunt ATM") 41 | 42 | self.autoTrip( 43 | self.TAKE_QUEST_MAPID, 44 | farm_resources_on_way=self.FARM_RESOURCES, 45 | callback=self.on_take_quest_map_reached 46 | ) 47 | 48 | def onTeleportToDistributorNearestZaap(self, code, err): 49 | """Handle teleport results.""" 50 | if code == UseTeleportItem.errors.CANT_USE_ITEM_IN_MAP: 51 | self.autoTrip( 52 | self.TAKE_QUEST_MAPID, 53 | farm_resources_on_way=self.FARM_RESOURCES, 54 | callback=self.on_take_quest_map_reached 55 | ) 56 | else: 57 | self.autoTrip( 58 | self.TAKE_QUEST_MAPID, 59 | self.TAKE_QUEST_ZONE_ID, 60 | farm_resources_on_way=self.FARM_RESOURCES, 61 | callback=self.on_take_quest_map_reached 62 | ) 63 | 64 | def on_take_quest_map_reached(self, code, err): 65 | """Handle arrival at the quest-taking location.""" 66 | if err: 67 | return self.finish(code, err) 68 | 69 | Logger().debug("Getting treasure hunt from distributor") 70 | self.use_skill( 71 | elementId=self.TREASURE_HUNT_ATM_IE_ID, 72 | skilluid=self.TREASURE_HUNT_ATM_SKILLUID, 73 | callback=self._on_treasure_hunt_taken 74 | ) 75 | 76 | def _on_treasure_hunt_taken(self, code, err): 77 | """Handle the result of taking the treasure hunt.""" 78 | if err: 79 | return self.finish(code, err) 80 | 81 | self.once(KernelEvent.TreasureHuntRequestAnswer, self.onTreasureHuntRequestAnswer) 82 | 83 | def onTreasureHuntRequestAnswer(self, event, code, err): 84 | """Process the treasure hunt request response.""" 85 | if code == TreasureHuntRequestEnum.TREASURE_HUNT_OK: 86 | self.on(KernelEvent.TreasureHuntUpdate, self.onQuestInfos) 87 | else: 88 | self.finish(self.errors.UNABLE_TO_TAKE_QUEST, f"Failed to take treasure hunt quest: {err}") 89 | 90 | 91 | def onQuestInfos(self, event, questType: int): 92 | if questType == TreasureHuntTypeEnum.TREASURE_HUNT_CLASSIC: 93 | self.finish(0, None) 94 | else: 95 | return self.finish(self.errors.UNSUPPORTED_HUNT_TYPE, f"Unsupported treasure hunt type : {questType}") 96 | 97 | @property 98 | def currentMapId(self): 99 | """Get the current map ID.""" 100 | return PlayedCharacterManager().currentMap.mapId -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/quest/treasure_hunt/TreasureHuntPoiDatabase.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pydofus2.com.ankamagames.dofus.datacenter.world.MapPosition import MapPosition 4 | 5 | class TreasureHuntPoiDatabase: 6 | """Maintains exact same POI database logic as original implementation.""" 7 | 8 | def __init__(self, hints_file: str, wrong_answers_file: str): 9 | self.hints_file = hints_file 10 | self.wrong_answers_file = wrong_answers_file 11 | 12 | # Load exactly as before 13 | with open(self.hints_file, "r") as fp: 14 | self.hint_db = json.load(fp) 15 | 16 | with open(self.wrong_answers_file, "r") as fp: 17 | json_content = json.load(fp) 18 | self.wrong_answers = set([tuple(_) for _ in json_content["recordedWrongAnswers"]]) 19 | 20 | def save_hints(self): 21 | """Save hints exactly as original implementation.""" 22 | with open(self.hints_file, "w") as fp: 23 | json.dump(self.hint_db, fp, indent=2) 24 | 25 | def save_wrong_answers(self): 26 | """Save wrong answers exactly as original implementation.""" 27 | with open(self.wrong_answers_file, "w") as fp: 28 | json.dump({"recordedWrongAnswers": list(self.wrong_answers)}, fp, indent=4) 29 | 30 | def memorize_hint(self, mapId, poiId): 31 | """Exact same logic as original memorizeHint.""" 32 | mp = MapPosition.getMapPositionById(mapId) 33 | if str(mp.worldMap) not in self.hint_db: 34 | self.hint_db[str(mp.worldMap)] = {} 35 | worldHints = self.hint_db[str(mp.worldMap)] 36 | if str(mp.id) not in worldHints: 37 | self.hint_db[str(mp.worldMap)][str(mp.id)] = [] 38 | self.hint_db[str(mp.worldMap)][str(mp.id)].append(poiId) 39 | self.save_hints() 40 | 41 | def remove_poi_from_map(self, mapId, poiId): 42 | """Exact same logic as original removePoiFromMap.""" 43 | mp = MapPosition.getMapPositionById(mapId) 44 | if str(mp.worldMap) not in self.hint_db: 45 | return 46 | worldHints = self.hint_db[str(mp.worldMap)] 47 | if str(mp.id) not in worldHints: 48 | return 49 | mapHints = [_ for _ in worldHints[str(mp.id)] if _ != poiId] 50 | self.hint_db[str(mp.worldMap)][str(mp.id)] = mapHints 51 | self.save_hints() 52 | 53 | def is_poi_in_map(self, mapId, poiId): 54 | """Exact same logic as original isPoiInMap.""" 55 | mp = MapPosition.getMapPositionById(mapId) 56 | if str(mp.worldMap) not in self.hint_db: 57 | return False 58 | worldHints = self.hint_db[str(mp.worldMap)] 59 | if str(mp.id) not in worldHints: 60 | return False 61 | mapHints: list = worldHints[str(mp.id)] 62 | return poiId in mapHints 63 | 64 | def add_wrong_answer(self, answer): 65 | """ 66 | Add a wrong answer to the database and save it immediately. 67 | 68 | Args: 69 | answer: Tuple of (startMapId, poiLabel, currentMapId) 70 | """ 71 | self.wrong_answers.add(answer) 72 | with open(self.wrong_answers_file, "w") as fp: 73 | json.dump({"recordedWrongAnswers": list(self.wrong_answers)}, fp, indent=4) -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/quest/treasure_hunt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/quest/treasure_hunt/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/server/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/skill/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/skill/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/start/ChangeServer.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 4 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 5 | from pydofus2.com.ankamagames.berilia.managers.KernelEventsManager import \ 6 | KernelEventsManager 7 | from pydofus2.com.ankamagames.dofus.kernel.net.ConnectionsHandler import \ 8 | ConnectionsHandler 9 | from pydofus2.com.ankamagames.dofus.kernel.net.DisconnectionReasonEnum import \ 10 | DisconnectionReasonEnum 11 | from pydofus2.com.ankamagames.dofus.logic.connection.managers.AuthenticationManager import \ 12 | AuthenticationManager 13 | from pydofus2.com.ankamagames.dofus.network.messages.game.approach.ReloginTokenRequestMessage import \ 14 | ReloginTokenRequestMessage 15 | from pydofus2.com.ankamagames.jerakine.benchmark.BenchmarkTimer import \ 16 | BenchmarkTimer 17 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 18 | 19 | if TYPE_CHECKING: 20 | pass 21 | 22 | class ChangeServer(AbstractBehavior): 23 | 24 | def __init__(self) -> None: 25 | super().__init__() 26 | self.requestTimer = None 27 | 28 | def run(self, newServerId) -> bool: 29 | self.newServerId = newServerId 30 | Logger().info("[ChangeServer] Started.") 31 | self.reloginTokenListener = KernelEventsManager().once(KernelEvent.ReloginToken, self.onReloginToken, originator=self) 32 | self.requestReloginToken() 33 | 34 | def onReloginToken(self, valid, token): 35 | if self.requestTimer: 36 | self.requestTimer.cancel() 37 | if not valid: 38 | return self.finish(False, "Received non valid token") 39 | self.reloginTokenListener = None 40 | AuthenticationManager()._lva.serverId = self.newServerId 41 | AuthenticationManager().setToken(token) 42 | ConnectionsHandler().closeConnection(DisconnectionReasonEnum.CHANGING_SERVER) 43 | self.finish(True, None, token=token) 44 | 45 | def finish(self, status, error=None, token=None): 46 | if self.reloginTokenListener: 47 | KernelEventsManager().removeListener(KernelEvent.ReloginToken, self.reloginTokenListener) 48 | super().finish(status, error, token=token) 49 | 50 | def requestReloginToken(self): 51 | def onTimeout(): 52 | self.finish(False, "Request login token timedout") 53 | self.requestTimer = BenchmarkTimer(5, onTimeout) 54 | rtrccmsg = ReloginTokenRequestMessage() 55 | rtrccmsg.init() 56 | self.requestTimer.start() 57 | ConnectionsHandler().send(rtrccmsg) 58 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/start/DeleteCharacter.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | from hashlib import md5 5 | 6 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 7 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 8 | from pydofus2.com.ankamagames.berilia.managers.KernelEventsManager import \ 9 | KernelEventsManager 10 | from pydofus2.com.ankamagames.dofus.kernel.net.ConnectionsHandler import \ 11 | ConnectionsHandler 12 | from pydofus2.com.ankamagames.dofus.network.messages.game.character.deletion.CharacterDeletionPrepareRequestMessage import \ 13 | CharacterDeletionPrepareRequestMessage 14 | from pydofus2.com.ankamagames.dofus.network.messages.game.character.deletion.CharacterDeletionRequestMessage import \ 15 | CharacterDeletionRequestMessage 16 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 17 | 18 | 19 | class DeleteCharacter(AbstractBehavior): 20 | 21 | def __init__(self) -> None: 22 | super().__init__() 23 | 24 | def run(self, characterId) -> bool: 25 | self.characterId = characterId 26 | Logger().info("[CreateNewCharacter] Started.") 27 | self.sendPrepareDeletionRequest(characterId) 28 | 29 | def sendPrepareDeletionRequest(self, characterId): 30 | msg = CharacterDeletionPrepareRequestMessage() 31 | msg.init(characterId) 32 | def onCharDelPrepared(event, msg): 33 | self.sendCharDeleteRequest(characterId) 34 | KernelEventsManager().on(KernelEvent.CharacterDelPrepare, onCharDelPrepared) 35 | ConnectionsHandler().send(msg) 36 | 37 | def sendCharDeleteRequest(self, characterId): 38 | cdrmsg = CharacterDeletionRequestMessage() 39 | answer = f"{characterId}~000000000000000000" 40 | answerhash = md5(answer.encode()).hexdigest() 41 | cdrmsg.init(characterId, answerhash) 42 | def oncharList(event, return_value): 43 | Logger().info(f"Characters list : {[(c.id, c.name) for c in return_value]}") 44 | for c in return_value: 45 | if c == characterId: 46 | return self.finish(False, "Character wasent deleted and still in chars list") 47 | self.finish(0) 48 | KernelEventsManager().once(KernelEvent.CharactersList, oncharList, originator=self) 49 | ConnectionsHandler().send(cdrmsg) -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/start/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/start/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/teleport/SaveZaap.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 2 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 3 | from pydofus2.com.ankamagames.dofus.internalDatacenter.taxi.TeleportDestinationWrapper import \ 4 | TeleportDestinationWrapper 5 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 6 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 7 | PlayedCharacterManager 8 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 9 | 10 | 11 | class SaveZaap(AbstractBehavior): 12 | ZAAP_IE_NOTFOUND = 255555 13 | 14 | def __init__(self) -> None: 15 | super().__init__() 16 | 17 | def run(self) -> bool: 18 | if int(Kernel().zaapFrame.spawnMapId) == int(PlayedCharacterManager().currentMap.mapId): 19 | Logger().debug(f"Zaap already saved in current map {PlayedCharacterManager().currentMap.mapId}.") 20 | return self.finish(0) 21 | if Kernel().interactiveFrame: 22 | self.openCurrMapZaapDialog() 23 | else: 24 | self.once_frame_pushed("RoleplayInteractivesFrame", self.openCurrMapZaapDialog) 25 | 26 | def openCurrMapZaapDialog(self): 27 | self.zaapIe = Kernel().interactiveFrame.getZaapIe() 28 | if not self.zaapIe: 29 | return self.finish(self.ZAAP_IE_NOTFOUND, "Zaap ie not found in current Map") 30 | self.once( 31 | event_id=KernelEvent.TeleportDestinationList, 32 | callback=self.onTeleportDestinationList, 33 | ) 34 | self.use_skill(ie=self.zaapIe, waitForSkillUsed=False, callback=self.onZaapSkillUsed) 35 | 36 | def onZaapSkillUsed(self, code, err): 37 | if err: 38 | return self.finish(code, err) 39 | 40 | def onZaapSaveResp(self, event_id, destinations: list[TeleportDestinationWrapper], ttype): 41 | self.close_dialog(lambda *_: self.finish(0)) 42 | 43 | def onTeleportDestinationList(self, event_id, destinations: list[TeleportDestinationWrapper], ttype): 44 | Logger().debug(f"Zaap teleport destinations received.") 45 | Kernel().zaapFrame.zaapRespawnSaveRequest() 46 | return self.once( 47 | KernelEvent.TeleportDestinationList, 48 | self.onZaapSaveResp, 49 | ) 50 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/teleport/TeleportUsingHavenBag.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional 3 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 4 | from pyd2bot.logic.roleplay.behaviors.teleport.UseZaap import UseZaap 5 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 6 | from pydofus2.com.ankamagames.berilia.managers.KernelEventsManager import KernelEventsManager 7 | from pydofus2.com.ankamagames.dofus.logic.common.managers.PlayerManager import PlayerManager 8 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import PlayedCharacterManager 9 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 10 | 11 | class TeleportUsingHavenbag(AbstractBehavior): 12 | 13 | class errors(Enum): 14 | INSUFFICIENT_LEVEL = 555112 15 | BASIC_ACCOUNT = 555113 16 | 17 | def __init__(self, dst_map_id) -> None: 18 | super().__init__() 19 | self.dst_map_id = dst_map_id 20 | 21 | def run(self): 22 | if not self.checkCanUse(): 23 | return 24 | 25 | self.toggle_haven_bag(wanted_state=True, callback=self.onInsideHavenbag) 26 | 27 | def checkCanUse(self) -> bool: 28 | if PlayerManager().isBasicAccount(): 29 | self.finish(self.errors.BASIC_ACCOUNT, "Cannot use havenbag - basic account") 30 | return False 31 | if PlayedCharacterManager().infos.level < 10: 32 | self.finish(self.errors.INSUFFICIENT_LEVEL, "Cannot use havenbag - insufficient level") 33 | return False 34 | return True 35 | 36 | 37 | def onInsideHavenbag(self, code: int, err: Optional[str]) -> None: 38 | if err: 39 | return self.finish(code, err) 40 | 41 | self.useZaap( 42 | self.dst_map_id, 43 | callback=self.onDestinationReached 44 | ) 45 | 46 | def onDestinationReached(self, code: int, err: Optional[str]) -> None: 47 | if err: 48 | if code == UseZaap.DST_ZAAP_NOT_KNOWN: 49 | def onHavenbagClosed(code2, err2): 50 | if err2: 51 | KernelEventsManager().send( 52 | KernelEvent.ClientRestart, 53 | f"Failed to close havenbag for reason {code2} {err2} after, destination zaap was not found in possible destinations." 54 | ) 55 | return 56 | 57 | self.finish(code, err) 58 | 59 | return self.toggle_haven_bag(wanted_state=False, callback=onHavenbagClosed) 60 | 61 | return self.finish(code, err) 62 | 63 | Logger().debug(f"Successfully teleported to map {self.dst_map_id}") 64 | self.finish(0) -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/teleport/ToggleHavenBag.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from pyd2bot.logic.roleplay.behaviors.AbstractBehavior import AbstractBehavior 4 | from pyd2bot.data.enums import ServerNotificationEnum 5 | from pydofus2.com.ankamagames.atouin.HaapiEventsManager import HaapiEventsManager 6 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 7 | from pydofus2.com.ankamagames.dofus.kernel.Kernel import Kernel 8 | from pydofus2.com.ankamagames.dofus.logic.common.managers.PlayerManager import PlayerManager 9 | from pydofus2.com.ankamagames.dofus.logic.game.common.managers.PlayedCharacterManager import \ 10 | PlayedCharacterManager 11 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.MapMemoryManager import MapMemoryManager 12 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 13 | 14 | if TYPE_CHECKING: 15 | pass 16 | 17 | 18 | class ToggleHavenBag(AbstractBehavior): 19 | NEED_LVL_10 = 589049 20 | ONLY_SUBSCRIBED = 589048 21 | TIMEDOUT = 589047 22 | CANT_USE_IN_CURRENT_MAP = 589088 23 | ALREADY_IN = 589046 24 | 25 | TIMEOUT = 30 26 | MAX_RETRIES = 3 27 | 28 | def __init__(self) -> None: 29 | super().__init__() 30 | 31 | def onHeavenBagEnterTimeout(self): 32 | Logger().error("Haven bag enter timedout!") 33 | self.useEnterHavenBagShortcut() 34 | 35 | def onServerTextInfo(self, event, msgId, msgType, textId, text, params): 36 | if textId == ServerNotificationEnum.DOESNT_HAVE_LVL_FOR_HAVEN_BAG: 37 | self.finish(self.NEED_LVL_10, text) 38 | elif textId == ServerNotificationEnum.CANT_USE_HAVEN_BAG_FROM_CURRMAP: 39 | MapMemoryManager().register_havenbag_allowance(PlayedCharacterManager().currentMap.mapId, False) 40 | mp = PlayedCharacterManager().currMapPos 41 | Logger().debug(f"allowTpFrom: {mp.allowTeleportFrom}, allowTpEveryWhere: {mp.allowTeleportEverywhere}, allowTpTo: {mp.allowTeleportTo}") 42 | self.finish(self.CANT_USE_IN_CURRENT_MAP, text) 43 | 44 | def useEnterHavenBagShortcut(self): 45 | if not Kernel().roleplayContextFrame: 46 | return self.once_frame_pushed('RoleplayContextFrame', self.useEnterHavenBagShortcut) 47 | HaapiEventsManager().registerShortcutUse("openHavenbag") 48 | Kernel().worker.terminated.wait(0.5) 49 | Kernel().roleplayContextFrame.havenbagEnter() 50 | 51 | def run(self, wanted_state=None) -> bool: 52 | if PlayedCharacterManager().infos.level < 10: 53 | return self.finish(self.NEED_LVL_10, "Need to be level 10 to use haven bag") 54 | if PlayerManager().isBasicAccount(): 55 | return self.finish(self.ONLY_SUBSCRIBED, "Only subscribed accounts can use haven bag") 56 | if wanted_state is not None: 57 | player_map_id = PlayedCharacterManager().currentMap.mapId 58 | if wanted_state: 59 | if PlayerManager().isMapInHavenbag(player_map_id): 60 | return self.finish(self.ALREADY_IN, "Already in havenbag") 61 | 62 | allow_tp = MapMemoryManager().is_havenbag_allowed(player_map_id) 63 | if allow_tp is not None and not allow_tp: 64 | return self.finish(self.CANT_USE_IN_CURRENT_MAP, "Havenbag access from current map is not allowed!") 65 | 66 | elif not wanted_state and not PlayerManager().isMapInHavenbag(PlayedCharacterManager().currentMap.mapId): 67 | return self.finish(self.ALREADY_IN, "Not in haven bag already") 68 | if wanted_state is not None: 69 | if wanted_state: 70 | self.havenBagListener = self.once( 71 | KernelEvent.InHavenBag, 72 | self.finish, 73 | timeout=self.TIMEOUT, 74 | retry_nbr=self.MAX_RETRIES, 75 | retry_action=self.onHeavenBagEnterTimeout, 76 | ontimeout=lambda _: self.finish(self.TIMEDOUT, "Haven bag enter timedout too many times"), 77 | ) 78 | else: 79 | self.once_map_rendered(lambda *_: self.finish(0)) 80 | self.on(KernelEvent.ServerTextInfo, self.onServerTextInfo) 81 | self.useEnterHavenBagShortcut() 82 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/teleport/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/teleport/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/behaviors/updates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/behaviors/updates/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/frames/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/frames/__init__.py -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/AutoTripEndedMessage.py: -------------------------------------------------------------------------------- 1 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 2 | 3 | 4 | class AutoTripEndedMessage(Message): 5 | def __init__(self, mapId): 6 | self.mapId = mapId 7 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/BankInteractionEndedMessage.py: -------------------------------------------------------------------------------- 1 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 2 | 3 | 4 | class BankInteractionEndedMessage(Message): 5 | def __init__(self): 6 | pass 7 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/BankUnloadEndedMessage.py: -------------------------------------------------------------------------------- 1 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 2 | 3 | 4 | class BankUnloadEndedMessage(Message): 5 | def __init__(self): 6 | pass 7 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/BankUnloadFailedMessage.py: -------------------------------------------------------------------------------- 1 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 2 | 3 | 4 | class BankUnloadFailedMessage(Message): 5 | def __init__(self) -> None: 6 | pass 7 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/ExchangeConcludedMessage.py: -------------------------------------------------------------------------------- 1 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 2 | 3 | 4 | class ExchangeConcludedMessage(Message): 5 | pass 6 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/FollowTransitionMessage.py: -------------------------------------------------------------------------------- 1 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.Transition import \ 2 | Transition 3 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 4 | 5 | 6 | class FollowTransitionMessage(Message): 7 | def __init__(self, transition: Transition, dstMapId: int): 8 | self.transition = transition 9 | self.dstMapId = dstMapId 10 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/MoveToVertexMessage.py: -------------------------------------------------------------------------------- 1 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.Vertex import \ 2 | Vertex 3 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 4 | 5 | 6 | class MoveToVertexMessage(Message): 7 | def __init__(self, Vertex: Vertex): 8 | self.vertex = Vertex 9 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/PhenixAutoReviveEndedMessage.py: -------------------------------------------------------------------------------- 1 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 2 | 3 | 4 | class PhenixAutoReviveEndedMessage(Message): 5 | 6 | def __init__(self): 7 | pass 8 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/SellerCollectedGuestItemsMessage.py: -------------------------------------------------------------------------------- 1 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 2 | 3 | 4 | class SellerCollectedGuestItemsMessage(Message): 5 | pass 6 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/SellerVacantMessage.py: -------------------------------------------------------------------------------- 1 | class SellerVacantMessage: 2 | 3 | def __init__(self, instanceId) -> None: 4 | self.instanceId = instanceId 5 | 6 | def __str__(self): 7 | return f"SellerVacant({self.instanceId})" -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/TakeNapMessage.py: -------------------------------------------------------------------------------- 1 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 2 | 3 | class TakeNapMessage(Message): 4 | """Message sent from leader to followers to coordinate nap times""" 5 | def __init__(self, nap_duration: int): 6 | super().__init__() 7 | if not isinstance(nap_duration, int) or nap_duration <= 0: 8 | raise ValueError(f"Invalid nap duration: {nap_duration}. Must be positive integer.") 9 | self.nap_duration = nap_duration 10 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/UnloadInsellerEndedMessage.py: -------------------------------------------------------------------------------- 1 | from pydofus2.com.ankamagames.jerakine.messages.Message import Message 2 | 3 | 4 | class UnloadInSellerEndedMessage(Message): 5 | pass 6 | -------------------------------------------------------------------------------- /pyd2bot/logic/roleplay/messages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/logic/roleplay/messages/__init__.py -------------------------------------------------------------------------------- /pyd2bot/misc/NapManager.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, TYPE_CHECKING 2 | from pydofus2.com.ankamagames.jerakine.benchmark.BenchmarkTimer import BenchmarkTimer 3 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 4 | from pydofus2.com.ankamagames.berilia.managers.KernelEvent import KernelEvent 5 | from pyd2bot.misc.BotEventsManager import BotEventsManager 6 | from pyd2bot.BotSettings import BotSettings 7 | 8 | if TYPE_CHECKING: 9 | from pyd2bot.Pyd2Bot import Pyd2Bot 10 | 11 | class NapManager: 12 | def __init__(self, client: "Pyd2Bot"): 13 | self.client = client 14 | self._taking_a_nap = False 15 | self._nap_duration: Optional[int] = None 16 | self._nap_take_timer = None 17 | self.logger = Logger() 18 | 19 | BotEventsManager().once( 20 | BotEventsManager.events.TAKE_NAP, 21 | self._on_nap_notification 22 | ) 23 | 24 | if not self.client.session.isMuleFighter: 25 | self._schedule_next_nap() 26 | 27 | def _schedule_next_nap(self) -> None: 28 | nap_timeout = BotSettings.generate_random_nap_timeout() * 60 * 60 # Convert hours to seconds 29 | self._nap_take_timer = BenchmarkTimer(int(nap_timeout), self._initiate_nap) 30 | BotEventsManager().send(BotEventsManager.events.TimeToNextNap, int(nap_timeout)) 31 | self._nap_take_timer.start() 32 | 33 | def _on_nap_notification(self, event, nap_duration: int) -> None: 34 | """Handle incoming nap notifications from leader""" 35 | self.logger.info(f"[{self.client.name}] Received nap notification for {nap_duration} minutes") 36 | self._nap_duration = nap_duration 37 | self._taking_a_nap = True 38 | self._send_update_to_front() 39 | self._handle_nap_start(nap_duration) 40 | 41 | def _handle_nap_start(self, nap_duration: Optional[int] = None) -> None: 42 | if nap_duration is None: 43 | nap_duration = BotSettings.generate_random_nap_duration() 44 | 45 | if self.client._main_behavior: 46 | self.client._main_behavior.stop() 47 | 48 | self.client.onReconnect( 49 | None, 50 | f"Taking a nap for {nap_duration} minutes", 51 | afterTime=int(nap_duration * 60) 52 | ) 53 | 54 | def _initiate_nap(self) -> None: 55 | self._taking_a_nap = True 56 | self._nap_duration = BotSettings.generate_random_nap_duration() 57 | self._send_update_to_front() 58 | 59 | if hasattr(self.client.session, 'followers') and self.client.session.followers: 60 | self._notify_followers() 61 | 62 | self._handle_nap_start(self._nap_duration) 63 | 64 | def _notify_followers(self) -> None: 65 | for follower in self.client.session.followers: 66 | follower_events = BotEventsManager.getInstance(follower.accountId) 67 | if follower_events: 68 | follower_events.send(BotEventsManager.events.TAKE_NAP, self._nap_duration) 69 | 70 | def _send_update_to_front(self) -> None: 71 | if self.client._stats_collector: 72 | self.client._stats_collector.sessionStats.timeSpentSleeping += self._nap_duration 73 | self.client._stats_collector.sessionStats.isSleeping = True 74 | self.client._stats_collector.onPlayerUpdate(KernelEvent.Paused) 75 | 76 | def is_napping(self) -> bool: 77 | return self._taking_a_nap 78 | 79 | def get_nap_duration(self) -> Optional[int]: 80 | return self._nap_duration 81 | 82 | def reset(self) -> None: 83 | self._taking_a_nap = False 84 | self._nap_duration = None 85 | if self._nap_take_timer: 86 | self._nap_take_timer.cancel() -------------------------------------------------------------------------------- /pyd2bot/misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/pyd2bot/misc/__init__.py -------------------------------------------------------------------------------- /pyd2bot/misc/areaInfos.json: -------------------------------------------------------------------------------- 1 | { 2 | "0": { 3 | "bank": ["Amakna"], 4 | "phoenix": { 5 | "mapId": 88086283.0 6 | } 7 | }, 8 | "5": { 9 | "bank": ["Suffokia"] 10 | }, 11 | "6": { 12 | "bank": ["Astrub"], 13 | "phoenix": { 14 | "mapId": 190843392 15 | } 16 | }, 17 | "18": { 18 | "bank": ["Astrub"], 19 | "phoenix": { 20 | "mapId": 190843392 21 | } 22 | }, 23 | "45": {}, 24 | "7": { 25 | "bank": ["Bonta"] 26 | }, 27 | "8": { 28 | "bank": ["Bonta"], 29 | "phoenix": { 30 | "mapId": 128059398.0 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /pyd2bot/misc/banks.json: -------------------------------------------------------------------------------- 1 | { 2 | "Astrub": { 3 | "name": "Astrub_bank", 4 | "npcMapId": 192415750.0, 5 | "npcActionId": 3, 6 | "npcId": -20001, 7 | "openBankReplyId": 64361 8 | }, 9 | "Bonta": { 10 | "name": "Bonta_bank", 11 | "npcMapId": 217059328.0, 12 | "npcActionId": 3, 13 | "npcId": -20000, 14 | "openBankReplyId": 63535 15 | }, 16 | "Suffokia": { 17 | "name": "Suffokia_bank", 18 | "npcMapId": 91753985.0, 19 | "npcActionId": 3, 20 | "npcId": -20000, 21 | "openBankReplyId": 64367 22 | }, 23 | "Amakna": { 24 | "name": "Amakna_bank", 25 | "npcMapId": 99095051.0, 26 | "npcActionId": 3, 27 | "npcId": -20000, 28 | "openBankReplyId": 64367 29 | }, 30 | "Pandala": { 31 | "name": "Pandala_bank", 32 | "npcMapId": 99095051.0, 33 | "npcActionId": 3, 34 | "npcId": -20000, 35 | "openBankReplyId": 64367 36 | }, 37 | "Frighost": { 38 | "name": "Frifri_bank", 39 | "npcMapId": 54534165.0, 40 | "npcActionId": 3, 41 | "npcId": -20000, 42 | "openBankReplyId": 64367 43 | }, 44 | "Koalak": { 45 | "name": "Koalak_bank", 46 | "npcMapId": 84935175.0, 47 | "npcActionId": 3, 48 | "npcId": -20000, 49 | "openBankReplyId": 64367 50 | } 51 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 119 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pydantic 3 | pydofus2 4 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/scripts/__init__.py -------------------------------------------------------------------------------- /scripts/fetch_account_data.py: -------------------------------------------------------------------------------- 1 | 2 | from pyd2bot.logic.managers.AccountManager import AccountManager 3 | 4 | account_key = "your_account_key" 5 | game = 1 # 1 = Dofus 6 | apikey = 'your_api_key' 7 | certificate_id = 'your_certificate_id' 8 | certificate_hash = "your_certificate_hash" 9 | account_data = AccountManager.fetch_account(game, apikey, int(certificate_id), certificate_hash) 10 | print(account_data) -------------------------------------------------------------------------------- /scripts/fetch_all_acounts.py: -------------------------------------------------------------------------------- 1 | from pyd2bot.logic.managers.AccountManager import AccountManager 2 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 3 | 4 | 5 | Logger.logToConsole = True 6 | AccountManager.clear() 7 | AccountManager.import_launcher_accounts(fetch_characters=True, save_to_local_json=True) 8 | -------------------------------------------------------------------------------- /scripts/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/scripts/icon.png -------------------------------------------------------------------------------- /scripts/icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadamrd/pyd2bot/9e96ff24341ae9f749527ff38b790cf6c3e58f69/scripts/icon2.png -------------------------------------------------------------------------------- /scripts/run_group_fight.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | 5 | from PyQt5 import QtGui, QtWidgets 6 | from pyd2bot.data.models import Path, PathTypeEnum, Session, SessionTypeEnum, UnloadTypeEnum 7 | from pydofus2.com.ankamagames.dofus.modules.utils.pathFinding.world.TransitionTypeEnum import TransitionTypeEnum 8 | from system_tray import SystemTrayIcon 9 | 10 | from pyd2bot.logic.managers.AccountManager import AccountManager 11 | from pyd2bot.Pyd2Bot import Pyd2Bot 12 | 13 | 14 | ankarnam_lvl1 = 154010883 15 | ankarnal_lvl5 = 154010884 16 | village_astrub = 191106048 17 | ankama_coin_bouftou = 88082704 18 | cania_pleines_rocheuses = 156240386 19 | currdir = os.path.dirname(os.path.abspath(__file__)) 20 | 21 | paths_file = os.path.join(os.path.dirname(__file__), "..", "app", "db", "paths.json") 22 | with open(paths_file, "r") as f: 23 | paths_json = json.load(f) 24 | paths: dict[str, Path] = {json_path["id"]: Path(**json_path) for json_path in paths_json} 25 | 26 | if __name__ == "__main__": 27 | # Logger.logToConsole = True 28 | app = QtWidgets.QApplication(sys.argv) 29 | app.setQuitOnLastWindowClosed(False) 30 | bots = [] 31 | followers_chars = [] 32 | farmer = {"account_key": 244588168247577056, "character_id": 829955506469} 33 | farmer_creds = AccountManager.get_credentials(farmer["account_key"], farmer["character_id"]) 34 | session_dict = { 35 | "id": farmer_creds["character"].accountId, 36 | "character": farmer_creds["character"], 37 | "unloadType": UnloadTypeEnum.BANK.value, 38 | "type": SessionTypeEnum.MULTIPLE_PATHS_FARM.value, 39 | "pathsList": [paths["Cania_Plains_Mine"], paths["Astrub_Mine"], paths["Dyna_Mine"], paths["Korussant_Mine"]], 40 | "apikey": farmer_creds["apikey"], 41 | "cert": farmer_creds["cert"] 42 | } 43 | bot = Pyd2Bot(Session(**session_dict)) 44 | bots.append(bot) 45 | leader = { 46 | "account_key": 244588168247577395, "character_id": 710703972643 47 | } 48 | leader_creds = AccountManager.get_credentials(leader["account_key"], leader["character_id"]) 49 | 50 | followers = [ 51 | { 52 | "account_key": 244588168247578055, "character_id": 710798475555 53 | }, 54 | { 55 | "account_key": 244588168247577841, "character_id": 710798344483 56 | }, 57 | { 58 | "account_key": 244588168247577629, "character_id": 710798278947 59 | } 60 | ] 61 | for player in followers: 62 | creds = AccountManager.get_credentials(player["account_key"], player["character_id"]) 63 | session_dict = { 64 | "id": creds["character"].accountId, 65 | "character": creds["character"], 66 | "unloadType": UnloadTypeEnum.BANK.value, 67 | "type": SessionTypeEnum.MULE_FIGHT.value, 68 | "leader": leader_creds["character"], 69 | "apikey": creds["apikey"], 70 | "cert": creds["cert"] 71 | } 72 | followers_chars.append(creds["character"]) 73 | bot = Pyd2Bot(Session(**session_dict)) 74 | bots.append(bot) 75 | 76 | session_dict = { 77 | "id": leader_creds["character"].accountId, 78 | "character": leader_creds["character"], 79 | "unloadType": UnloadTypeEnum.BANK.value, 80 | "type": SessionTypeEnum.FIGHT.value, 81 | "followers": followers_chars, 82 | "path": paths["astrub_village"], 83 | "apikey": leader_creds["apikey"], 84 | "cert": leader_creds["cert"] 85 | } 86 | bot = Pyd2Bot(Session(**session_dict)) 87 | bots.append(bot) 88 | icon = QtGui.QIcon(os.path.join(currdir, "icon.png")) 89 | trayIcon = SystemTrayIcon(icon, bots) 90 | for bot in bots: 91 | bot.start() 92 | trayIcon.show() 93 | sys.exit(app.exec_()) 94 | -------------------------------------------------------------------------------- /scripts/run_solofight_bot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from PyQt5 import QtGui, QtWidgets 5 | from pyd2bot.data.models import Session, SessionTypeEnum, UnloadTypeEnum 6 | from pyd2bot.farmPaths.PathManager import PathManager 7 | from pydofus2.com.ankamagames.jerakine.logger.Logger import Logger 8 | from system_tray import SystemTrayIcon 9 | 10 | from pyd2bot.logic.managers.AccountManager import AccountManager 11 | from pyd2bot.Pyd2Bot import Pyd2Bot 12 | 13 | 14 | currdir = os.path.dirname(os.path.abspath(__file__)) 15 | 16 | if __name__ == "__main__": 17 | AccountManager.load() 18 | app = QtWidgets.QApplication(sys.argv) 19 | app.setQuitOnLastWindowClosed(False) 20 | accountId = 178805104 21 | characterId = 711154991395.0 22 | account = AccountManager.get_account(accountId) 23 | character = account.get_character(characterId) 24 | if not character: 25 | raise Exception("character not found") 26 | session = Session( 27 | **{ 28 | "id": "fight_solo", 29 | "unloadType": UnloadTypeEnum.BANK.value, 30 | "type": SessionTypeEnum.SOLO_FIGHT.value, 31 | "path": PathManager.get_path("Astrub_City"), 32 | "monsterLvlCoefDiff": 1.5, 33 | "credentials": account.credentials, 34 | "character": character, 35 | } 36 | ) 37 | bot = Pyd2Bot(session) 38 | bot.start() 39 | icon = QtGui.QIcon(os.path.join(currdir, "icon.png")) 40 | trayIcon = SystemTrayIcon(icon, [bot], title=character.name) 41 | 42 | def onShutdown(message, reason): 43 | print(f"Shutting down {character.name} because {reason}, details:\n{message}") 44 | QtWidgets.QApplication.quit() 45 | 46 | bot.addShutdownListener(onShutdown) 47 | trayIcon.show() 48 | sys.exit(app.exec_()) 49 | -------------------------------------------------------------------------------- /scripts/run_treasureHuntBot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from scripts.system_tray import SystemTrayIcon 5 | from PyQt5 import QtGui, QtWidgets 6 | 7 | from pyd2bot.logic.managers.AccountManager import AccountManager 8 | from pyd2bot.Pyd2Bot import Pyd2Bot 9 | from pyd2bot.data.models import Session, SessionTypeEnum, UnloadTypeEnum 10 | 11 | 12 | __dir__ = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | if __name__ == "__main__": 15 | account_key = "244588168235074572" 16 | creds = AccountManager.get_credentials(account_key) 17 | session = Session( 18 | id="account_key", 19 | character=creds['character'], 20 | unloadType=UnloadTypeEnum.BANK, 21 | type=SessionTypeEnum.TREASURE_HUNT, 22 | apikey=creds['apikey'], 23 | cert=creds['cert'], 24 | ) 25 | app = QtWidgets.QApplication(sys.argv) 26 | app.setQuitOnLastWindowClosed(False) 27 | bot = Pyd2Bot(session) 28 | bot.start() 29 | bot.addShutdownListener(lambda accountId, reason, message: QtWidgets.QApplication.quit()) 30 | icon = QtGui.QIcon(os.path.join(__dir__, "icon.png")) 31 | trayIcon = SystemTrayIcon(icon, bot) 32 | trayIcon.show() 33 | sys.exit(app.exec_()) -------------------------------------------------------------------------------- /scripts/system_tray.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtWidgets 2 | from pyd2bot.Pyd2Bot import Pyd2Bot 3 | 4 | 5 | class SystemTrayIcon(QtWidgets.QSystemTrayIcon): 6 | 7 | def __init__(self, icon, bots: list[Pyd2Bot], parent=None, title="bots"): 8 | super(SystemTrayIcon, self).__init__(icon, parent) 9 | self.setToolTip(title) 10 | 11 | menu = QtWidgets.QMenu(parent) 12 | exit_action = menu.addAction("Stop Bot") 13 | exit_action.triggered.connect(self.stop_bot) 14 | self.bots = bots 15 | self.setContextMenu(menu) 16 | 17 | def stop_bot(self): 18 | print("Stopping the bots ...") 19 | for bot in self.bots: 20 | bot.shutdown("User wanted to stop the bot") 21 | for bot in self.bots: 22 | bot.join() 23 | QtWidgets.QApplication.quit() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | # Application name: 5 | name="pyd2bot", 6 | # Version number (initial): 7 | version=open("./VERSION").read(), 8 | # Application author details: 9 | author="majdoub khalid", 10 | author_email="majdoub.khalid@gmail.com", 11 | # Packages 12 | packages=find_packages(), 13 | # Include additional files into the package 14 | include_package_data=True, 15 | # Details 16 | url="https://github.com/hadamrd/pyd2bot", 17 | # 18 | # license="LICENSE.txt", 19 | description="Python bot that uses pydofus2 as a background client.", 20 | long_description=open("README.md").read(), 21 | # Dependent packages (distributions) 22 | install_requires=open("./requirements.txt").readlines(), 23 | classifiers=[ 24 | "Programming Language :: Python :: 3.9.11", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: Windows", 27 | ], 28 | python_requires="<=3.9.13", 29 | ) 30 | -------------------------------------------------------------------------------- /todos: -------------------------------------------------------------------------------- 1 | *) 2 | make auto trip take a save zaap flag 3 | when its true bot will reach destination and before ending will 4 | look for nearest zaap to its current vertex and walk to it to save it 5 | then finish --------------------------------------------------------------------------------