├── logs └── .gitkeep ├── gui ├── __init__.py ├── dev.py └── custom.py ├── lib ├── __init__.py ├── logger.py ├── maps.py ├── shared.py ├── convert.py ├── accounts.py ├── settings.py ├── parser.py ├── imgcompare.py ├── data.py └── tools.py ├── threads ├── __init__.py ├── base.py ├── travel.py ├── bot.py ├── job.py ├── game.py └── farming.py ├── pyautogui ├── _pyautogui_java.py ├── __main__.py ├── _pyautogui_osx.py ├── _pyautogui_x11.py └── _pyautogui_win.py ├── icons ├── hand.png ├── logo.png ├── zaap.png ├── arrow.png ├── drago.png ├── dragox2.png ├── enclos.png ├── loader.gif ├── miner.png ├── scroll.png ├── crosshair.png ├── hourglass.png └── destination.png ├── paths ├── test_connect_disconnect.path ├── test.path ├── test_collect.path ├── 2.46 │ ├── solo │ │ ├── enclos_brak.path │ │ └── enclos_bonta.path │ └── enclos_bonta_brak.path └── store │ ├── from_[7,-23]_to_bank_astrub.path │ └── from_[9,-23]_to_bank_astrub.path ├── screenshot.png ├── .gitignore ├── bot.py ├── dependencies.txt ├── uninstall.sh ├── dindo-bot.desktop ├── dindo-bot-dev.desktop ├── .github ├── FUNDING.yml └── stale.yml ├── LICENSE ├── todo.md ├── install.sh ├── maps.data ├── README.md └── pyscreeze └── __init__.py /logs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /threads/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyautogui/_pyautogui_java.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/hand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/hand.png -------------------------------------------------------------------------------- /icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/logo.png -------------------------------------------------------------------------------- /icons/zaap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/zaap.png -------------------------------------------------------------------------------- /paths/test_connect_disconnect.path: -------------------------------------------------------------------------------- 1 | Connect(account_id=1) 2 | Disconnect(False) 3 | -------------------------------------------------------------------------------- /pyautogui/__main__.py: -------------------------------------------------------------------------------- 1 | from . import displayMousePosition 2 | displayMousePosition() -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/screenshot.png -------------------------------------------------------------------------------- /icons/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/arrow.png -------------------------------------------------------------------------------- /icons/drago.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/drago.png -------------------------------------------------------------------------------- /icons/dragox2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/dragox2.png -------------------------------------------------------------------------------- /icons/enclos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/enclos.png -------------------------------------------------------------------------------- /icons/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/loader.gif -------------------------------------------------------------------------------- /icons/miner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/miner.png -------------------------------------------------------------------------------- /icons/scroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/scroll.png -------------------------------------------------------------------------------- /icons/crosshair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/crosshair.png -------------------------------------------------------------------------------- /icons/hourglass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/hourglass.png -------------------------------------------------------------------------------- /icons/destination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamerium/Dindo-Bot/HEAD/icons/destination.png -------------------------------------------------------------------------------- /paths/test.path: -------------------------------------------------------------------------------- 1 | Move(UP) 2 | Move(RIGHT) 3 | Move(DOWN) 4 | Move(DOWN) 5 | Move(LEFT) 6 | Move(UP) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | *.*~ 3 | logs/* 4 | icons/more/* 5 | dragodindes/* 6 | accounts.json 7 | settings.json 8 | 9 | # Python cache 10 | __pycache__/ 11 | *.pyc 12 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Dindo Bot 3 | # Copyright (c) 2018 - 2019 AXeL 4 | 5 | ''' 6 | Farming bot for Dofus game 7 | ''' 8 | 9 | from gui.main import BotWindow 10 | 11 | bot = BotWindow() 12 | bot.log('Bot window loaded') 13 | bot.main() 14 | -------------------------------------------------------------------------------- /paths/test_collect.path: -------------------------------------------------------------------------------- 1 | Collect(map=[7,-23],store_path=paths/store/from_[7,-23]_to_bank_astrub.path) 2 | Move(RIGHT) 3 | Move(RIGHT) 4 | Collect(map=[9,-23],store_path=paths/store/from_[9,-23]_to_bank_astrub.path) 5 | Move(DOWN) 6 | Move(LEFT) 7 | Move(UP) 8 | Move(LEFT) 9 | -------------------------------------------------------------------------------- /dependencies.txt: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | 3 | ### Linux 4 | 5 | # Xlib 6 | sudo apt install python-xlib 7 | # for python 3 8 | sudo apt install python3-xlib 9 | 10 | # Pillow 11 | sudo apt install python-imaging 12 | # or 13 | sudo apt install python-pil 14 | # for python 3 15 | sudo apt install python3-imaging 16 | # or 17 | sudo apt install python3-pil 18 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$(id -u)" != "0" ]; then 4 | echo “This script must be run as root” 2>&1 5 | exit 1 6 | fi 7 | 8 | # remove symlink 9 | rm -f /usr/local/bin/dindo-bot 10 | 11 | # remove desktop files 12 | rm -f /usr/share/applications/dindo-bot.desktop 13 | rm -f /usr/share/applications/dindo-bot-dev.desktop 14 | 15 | # remove icons 16 | rm -f /usr/share/icons/hicolor/128x128/apps/dindo-bot.png 17 | -------------------------------------------------------------------------------- /dindo-bot.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Encoding=UTF-8 4 | Name=Dindo Bot 5 | GenericName=Dindo Bot 6 | Comment=Farming bot for Dofus game 7 | #Icon=icons/logo.png 8 | Icon=/usr/share/icons/hicolor/128x128/apps/dindo-bot.png 9 | #Exec=bot.py 10 | Exec=sh -e -c "chmod +x \\"\\$(dirname \\"\\$0\\")/bot.py\\" && exec \\"\\$(dirname \\"\\$0\\")/bot.py\\"" %k 11 | Terminal=false 12 | StartupNotify=false 13 | Categories=Application;Game 14 | -------------------------------------------------------------------------------- /paths/2.46/solo/enclos_brak.path: -------------------------------------------------------------------------------- 1 | Zaap(from=Havenbag,to=Brakmar) 2 | Zaapi(from=Zaap Brakmar,to=Animal SH) 3 | Enclos(location=[-32,37],type=Energy) 4 | Move(DOWN) 5 | Enclos(location=[-32,38],type=NegativeSerenity) 6 | Move(RIGHT) 7 | Enclos(location=[-31,38],type=Endurance) 8 | Move(RIGHT) 9 | Enclos(location=[-30,38],type=PositiveSerenity) 10 | Move(UP) 11 | Enclos(location=[-30,37],type=Amour) 12 | Move(LEFT) 13 | Enclos(location=[-31,37],type=Maturity) 14 | -------------------------------------------------------------------------------- /dindo-bot-dev.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Encoding=UTF-8 4 | Name=Dindo Bot (dev mode) 5 | GenericName=Dindo Bot (dev mode) 6 | Comment=Farming bot for Dofus game 7 | #Icon=icons/logo.png 8 | Icon=/usr/share/icons/hicolor/128x128/apps/dindo-bot.png 9 | #Exec=bot.py 10 | Exec=sh -e -c "chmod +x \\"\\$(dirname \\"\\$0\\")/bot.py\\" && exec \\"\\$(dirname \\"\\$0\\")/bot.py\\" --dev" %k 11 | Terminal=false 12 | StartupNotify=false 13 | Categories=Application;Game 14 | -------------------------------------------------------------------------------- /paths/2.46/solo/enclos_bonta.path: -------------------------------------------------------------------------------- 1 | Zaap(from=Havenbag,to=Bonta) 2 | Zaapi(from=Zaap Bonta,to=Animal SH) 3 | Move(LEFT) 4 | Enclos(location=[-37,-56],type=Amour) 5 | Move(LEFT) 6 | Enclos(location=[-38,-56],type=Endurance) 7 | Move(UP) 8 | Enclos(location=[-38,-57],type=NegativeSerenity) 9 | Move(RIGHT) 10 | Enclos(location=[-37,-57],type=PositiveSerenity) 11 | Move(UP) 12 | Enclos(location=[-37,-58],type=Energy) 13 | Move(DOWN) 14 | Move(RIGHT) 15 | Enclos(location=[-36,-57],type=Maturity) 16 | -------------------------------------------------------------------------------- /lib/logger.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | from .tools import save_text_to_file, get_full_path, get_date, get_time 5 | 6 | def get_filename(): 7 | return get_full_path('logs/' + get_date() + '.log') 8 | 9 | def new_entry(text): 10 | filename = get_filename() 11 | new_text = '[' + get_time() + '] ' + text + '\n' 12 | save_text_to_file(new_text, filename, 'a') 13 | 14 | def debug(text): 15 | new_entry('DEBUG::' + text) 16 | 17 | def error(text): 18 | new_entry('ERROR::' + text) 19 | 20 | def add_separator(bold=False): 21 | filename = get_filename() 22 | separator = '=' if bold else '-' 23 | line = separator*50 + '\n' 24 | save_text_to_file(line, filename, 'a') 25 | -------------------------------------------------------------------------------- /lib/maps.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | import json 5 | from .tools import read_file, save_text_to_file, get_full_path 6 | 7 | def get_filename(): 8 | return get_full_path('maps.data') 9 | 10 | def to_string(data): 11 | return json.dumps(data) 12 | 13 | def to_array(text, jsonify=True): 14 | if jsonify: 15 | json_text = text.replace('\'', '"') 16 | else: 17 | json_text = text 18 | return json.loads(json_text) 19 | 20 | def load(): 21 | text = read_file(get_filename()) 22 | if text: 23 | data = to_array(text, False) 24 | else: 25 | data = {} 26 | return data 27 | 28 | def save(data): 29 | text = to_string(data) 30 | save_text_to_file(text, get_filename()) 31 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://www.paypal.me/axeldev 13 | -------------------------------------------------------------------------------- /lib/shared.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | __program_name__ = 'Dindo Bot' 5 | __program_desc__ = 'Farming bot for Dofus game' 6 | __website__ = 'https://github.com/AXeL-dev' 7 | __website_label__ = 'AXeL-dev' 8 | __version__ = '2.0.4' 9 | __authors__ = ['AXeL'] 10 | 11 | class LogType: 12 | Normal, Info, Success, Error = range(4) 13 | 14 | class DebugLevel: 15 | ''' 16 | High: The highest level (for text with too much details) 17 | Normal: Medium level (for text to show when the debug level is normal or high) 18 | Low: The lowest level (for text to always show when debug is enabled) 19 | ''' 20 | High, Normal, Low = range(3) 21 | 22 | class GameVersion: 23 | Retro, Two = 1, 2 24 | -------------------------------------------------------------------------------- /paths/store/from_[7,-23]_to_bank_astrub.path: -------------------------------------------------------------------------------- 1 | Move(DOWN) 2 | Move(DOWN) 3 | Move(LEFT) 4 | Move(LEFT) 5 | Move(DOWN) 6 | Move(LEFT) 7 | Move(DOWN) 8 | Move(DOWN) 9 | Click(x=565,y=212,width=900,height=700,twice=False) 10 | MonitorGameScreen() 11 | Click(x=471,y=234,width=900,height=700,twice=False) 12 | MonitorGameScreen() 13 | Click(x=413,y=509,width=900,height=700,twice=False) 14 | MonitorGameScreen() 15 | Click(x=661,y=89,width=900,height=700,twice=False) 16 | Wait(1) 17 | Click(x=726,y=130,width=900,height=700,twice=False) 18 | PressKey(esc) 19 | MonitorGameScreen() 20 | Click(x=240,y=460,width=900,height=700,twice=False) 21 | MonitorGameScreen() 22 | Move(UP) 23 | Move(UP) 24 | Move(UP) 25 | Move(UP) 26 | Move(UP) 27 | Move(RIGHT) 28 | Move(RIGHT) 29 | Move(RIGHT) 30 | -------------------------------------------------------------------------------- /paths/store/from_[9,-23]_to_bank_astrub.path: -------------------------------------------------------------------------------- 1 | Move(LEFT) 2 | Move(LEFT) 3 | Move(DOWN) 4 | Move(DOWN) 5 | Move(LEFT) 6 | Move(LEFT) 7 | Move(DOWN) 8 | Move(LEFT) 9 | Move(DOWN) 10 | Move(DOWN) 11 | Click(x=565,y=212,width=900,height=700,twice=False) 12 | MonitorGameScreen() 13 | Click(x=471,y=234,width=900,height=700,twice=False) 14 | MonitorGameScreen() 15 | Click(x=413,y=509,width=900,height=700,twice=False) 16 | MonitorGameScreen() 17 | Click(x=661,y=89,width=900,height=700,twice=False) 18 | Wait(1) 19 | Click(x=726,y=130,width=900,height=700,twice=False) 20 | PressKey(esc) 21 | MonitorGameScreen() 22 | Click(x=240,y=460,width=900,height=700,twice=False) 23 | MonitorGameScreen() 24 | Move(UP) 25 | Move(UP) 26 | Move(UP) 27 | Move(UP) 28 | Move(UP) 29 | Move(RIGHT) 30 | Move(RIGHT) 31 | Move(RIGHT) 32 | Move(RIGHT) 33 | Move(RIGHT) 34 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - bug 10 | - enhancement 11 | - todo 12 | - feature request 13 | # Label to use when marking an issue as stale 14 | staleLabel: wontfix 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /paths/2.46/enclos_bonta_brak.path: -------------------------------------------------------------------------------- 1 | Zaap(from=Havenbag,to=Brakmar) 2 | Zaapi(from=Zaap Brakmar,to=Animal SH) 3 | Enclos(location=[-32,37],type=Energy) 4 | Move(DOWN) 5 | Enclos(location=[-32,38],type=NegativeSerenity) 6 | Move(RIGHT) 7 | Enclos(location=[-31,38],type=Endurance) 8 | Move(RIGHT) 9 | Enclos(location=[-30,38],type=PositiveSerenity) 10 | Move(UP) 11 | Enclos(location=[-30,37],type=Amour) 12 | Move(LEFT) 13 | Enclos(location=[-31,37],type=Maturity) 14 | Zaap(from=Havenbag,to=Bonta) 15 | Zaapi(from=Zaap Bonta,to=Animal SH) 16 | Move(LEFT) 17 | Enclos(location=[-37,-56],type=Amour) 18 | Move(LEFT) 19 | Enclos(location=[-38,-56],type=Endurance) 20 | Move(UP) 21 | Enclos(location=[-38,-57],type=NegativeSerenity) 22 | Move(RIGHT) 23 | Enclos(location=[-37,-57],type=PositiveSerenity) 24 | Move(UP) 25 | Enclos(location=[-37,-58],type=Energy) 26 | Move(DOWN) 27 | Move(RIGHT) 28 | Enclos(location=[-36,-57],type=Maturity) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 AXeL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | ### TODO 2 | 3 | * Add plugins system (for example: fight handling can be embedded into a plugin & plugged if needed) 4 | * Use OCR to detect dd number in enclos/inventory 5 | * Add an XP system for dd 6 | * Display global dd stats in log text view or in a widget 7 | * Manage cowshed also, instead of enclos & inventory only 8 | * Add option to switch/change player 9 | * Use AI for rearing stuff 10 | * Zaap positions are not always the same, so use OCR to detect them maybe? instead of change them for each player, or find a solution for zaap/zaapi use on multi-accounts 11 | * Add treasure hunts management 12 | * Allow user to add more Enclos locations (do the same for Zaap & Zaapi) 13 | * Find a way/create an instruction to reverse path 14 | * Auto-detect & close annoying windows (level up, invite, etc..), use ESC to close (already done for level up but need to apply it on the rest) 15 | * Implement a more easy way to load/switch between maps on Map tab 16 | * Finish collect action (#TODO) 17 | * If podbar level didn't change after collect, we can consider that the resource was not collected 18 | * Add an update feature? (from github repo, or simply create a deb package who doesn't erase/update data files) 19 | * Add more bot state widgets (life widget for example)? 20 | * Add french translation 21 | -------------------------------------------------------------------------------- /lib/convert.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | import gi 5 | gi.require_version('Gtk', '3.0') 6 | from gi.repository import GLib, GdkPixbuf 7 | from PIL import Image 8 | 9 | # Convert GdkPixbuf to Pillow image 10 | def pixbuf2image(pixbuf): 11 | data = pixbuf.get_pixbufels() 12 | width = pixbuf.props.width 13 | height = pixbuf.props.height 14 | stride = pixbuf.props.rowstride 15 | mode = 'RGB' 16 | if pixbuf.props.has_alpha == True: 17 | mode = 'RGBA' 18 | image = Image.frombytes(mode, (width, height), data, 'raw', mode, stride) 19 | return image 20 | 21 | # Convert Pillow image to GdkPixbuf 22 | def image2pixbuf(image): 23 | data = image.tobytes() 24 | w, h = image.size 25 | data = GLib.Bytes.new(data) 26 | pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(data, GdkPixbuf.Colorspace.RGB, False, 8, w, h, w * 3) 27 | return pixbuf 28 | 29 | # Convert RGB color to GdkPixbuf pixel 30 | def rgb2pixel(rgb): 31 | red = rgb[0] << 24 32 | green = rgb[1] << 16 33 | blue = rgb[2] << 8 34 | color = red | green | blue | 255 35 | return color 36 | 37 | # Convert RGB color to HEX format 38 | def rgb2hex(rgb): 39 | hex = '#%02x%02x%02x' % rgb 40 | return hex 41 | 42 | # Convert HEX color to RGB format 43 | def hex2rgb(hex): 44 | hexcode = hex.lstrip('#') 45 | rgb = tuple(map(ord, hexcode.decode('hex'))) 46 | return rgb 47 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # root detection 4 | if [ "$(id -u)" != "0" ]; then 5 | echo “This script must be run as root” 2>&1 6 | exit 1 7 | fi 8 | 9 | # run uninstall script first 10 | sh uninstall.sh 11 | 12 | # package manager detection 13 | APT_CMD=$(which apt) 14 | PACMAN_CMD=$(which pacman) 15 | 16 | # python version detection 17 | #PYTHON_VER=$(python -c"import sys; print(sys.version_info.major)") 18 | PYTHON_VER="3" 19 | 20 | # install dependencies 21 | if [[ ! -z $APT_CMD ]]; then 22 | if [ $PYTHON_VER -eq 3 ]; then 23 | sudo apt -y install python3-gi gir1.2-gtk-3.0 gir1.2-wnck-3.0 python3-xlib python3-pil scrot 24 | else 25 | sudo apt -y install python-gi gir1.2-gtk-3.0 gir1.2-wnck-3.0 python-xlib python-pil scrot 26 | fi 27 | elif [[ ! -z $PACMAN_CMD ]]; then 28 | sudo pacman -S python-gobject gtk3 libwnck3 python-xlib python-pillow scrot 29 | else 30 | echo "error can't install dependencies" 31 | exit 1 32 | fi 33 | 34 | # get script path 35 | SCRIPT_PATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 36 | 37 | # create symlink 38 | ln -s $SCRIPT_PATH/bot.py /usr/local/bin/dindo-bot 39 | chmod 755 /usr/local/bin/dindo-bot 40 | 41 | # copy & update desktop files 42 | cp dindo-bot.desktop /usr/share/applications/dindo-bot.desktop 43 | sed -i 's/^Exec=.*$/Exec=dindo-bot/g' /usr/share/applications/dindo-bot.desktop 44 | chmod 755 /usr/share/applications/dindo-bot.desktop 45 | 46 | cp dindo-bot-dev.desktop /usr/share/applications/dindo-bot-dev.desktop 47 | sed -i 's/^Exec=.*$/Exec=dindo-bot --dev/g' /usr/share/applications/dindo-bot-dev.desktop 48 | chmod 755 /usr/share/applications/dindo-bot-dev.desktop 49 | 50 | # copy icons 51 | cp icons/logo.png /usr/share/icons/hicolor/128x128/apps/dindo-bot.png 52 | chmod 644 /usr/share/icons/hicolor/128x128/apps/dindo-bot.png 53 | 54 | # refresh icons cache 55 | gtk-update-icon-cache 56 | -------------------------------------------------------------------------------- /lib/accounts.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | import json 5 | from .tools import read_file, save_text_to_file, get_full_path 6 | 7 | def get_filename(): 8 | return get_full_path('accounts.json') 9 | 10 | def load(): 11 | text = read_file(get_filename()) 12 | if text: 13 | return json.loads(text) 14 | else: 15 | return [] 16 | 17 | def save(accounts): 18 | text = json.dumps(accounts) 19 | save_text_to_file(text, get_filename()) 20 | 21 | def get_next_id(accounts): 22 | if accounts: 23 | greater_id = 1 24 | for account in accounts: 25 | if account['id'] > greater_id: 26 | greater_id = account['id'] 27 | next_id = greater_id + 1 28 | else: 29 | next_id = 1 30 | return next_id 31 | 32 | def is_duplicate(login): 33 | accounts = load() 34 | for account in accounts: 35 | if account['login'] == login: 36 | return True 37 | return False 38 | 39 | def add(login, pwd): 40 | # add account 41 | accounts = load() 42 | id = get_next_id(accounts) 43 | position = id - 1 44 | accounts.append({ 45 | 'id': id, 46 | 'login': login, 47 | 'pwd': pwd, 48 | 'position': position 49 | }) 50 | # save 51 | save(accounts) 52 | return (id, accounts) 53 | 54 | def remove(id): 55 | # remove account 56 | accounts = load() 57 | for i, account in enumerate(accounts): 58 | if account['id'] == id: 59 | del accounts[i] 60 | break 61 | # save 62 | save(accounts) 63 | return accounts 64 | 65 | def get(id): 66 | accounts = load() 67 | for account in accounts: 68 | if account['id'] == id: 69 | return account 70 | return None 71 | 72 | def swap(id1, id2): 73 | accounts = load() 74 | account1_index, account2_index = (None, None) 75 | # get accounts indexes by id 76 | for i, account in enumerate(accounts): 77 | if account['id'] == id1 and account1_index is None: 78 | account1_index = i 79 | if account2_index is not None: # if we have the 2 indexes, break 80 | break 81 | elif account['id'] == id2 and account2_index is None: 82 | account2_index = i 83 | if account1_index is not None: # if we have the 2 indexes, break 84 | break 85 | # swap positions 86 | if account1_index is not None and account2_index is not None: 87 | account1_position = accounts[account1_index]['position'] 88 | accounts[account1_index]['position'] = accounts[account2_index]['position'] 89 | accounts[account2_index]['position'] = account1_position 90 | # save 91 | save(accounts) 92 | return accounts 93 | -------------------------------------------------------------------------------- /maps.data: -------------------------------------------------------------------------------- 1 | {"[7,-23]": [{"y": 87, "x": 554, "height": 700, "color": "(241, 190, 43)", "width": 900}, {"y": 113, "x": 604, "height": 700, "color": "(200, 169, 23)", "width": 900}, {"y": 137, "x": 572, "height": 700, "color": "(225, 207, 85)", "width": 900}, {"y": 103, "x": 528, "height": 700, "color": "(217, 172, 33)", "width": 900}, {"y": 123, "x": 493, "height": 700, "color": "(182, 155, 44)", "width": 900}, {"y": 149, "x": 515, "height": 700, "color": "(219, 194, 86)", "width": 900}, {"y": 378, "x": 340, "height": 700, "color": "(199, 194, 97)", "width": 900}, {"y": 396, "x": 311, "height": 700, "color": "(190, 159, 35)", "width": 900}, {"y": 419, "x": 271, "height": 700, "color": "(255, 235, 66)", "width": 900}, {"y": 436, "x": 324, "height": 700, "color": "(227, 165, 36)", "width": 900}, {"y": 424, "x": 325, "width": 900, "color": "(167, 174, 22)", "height": 700}], "[9,-23]": [{"y": 283, "x": 344, "height": 700, "color": "(238, 177, 35)", "width": 900}, {"y": 327, "x": 360, "height": 700, "color": "(208, 160, 31)", "width": 900}, {"y": 345, "x": 321, "height": 700, "color": "(221, 208, 116)", "width": 900}, {"y": 346, "x": 267, "height": 700, "color": "(232, 174, 31)", "width": 900}, {"y": 300, "x": 332, "height": 700, "color": "(185, 126, 19)", "width": 900}, {"y": 320, "x": 294, "height": 700, "color": "(255, 232, 59)", "width": 900}], "[5,-22]": [{"y": 130, "x": 263, "height": 700, "color": "(193, 159, 53)", "width": 900}, {"y": 138, "x": 144, "height": 700, "color": "(237, 177, 36)", "width": 900}, {"y": 126, "x": 220, "height": 700, "color": "(237, 192, 70)", "width": 900}, {"y": 136, "x": 342, "height": 700, "color": "(220, 177, 56)", "width": 900}, {"y": 159, "x": 173, "height": 700, "color": "(193, 160, 29)", "width": 900}, {"y": 116, "x": 215, "height": 700, "color": "(231, 194, 79)", "width": 900}, {"y": 145, "x": 292, "height": 700, "color": "(188, 142, 51)", "width": 900}], "[8, -24]": [{"x": 494, "y": 236, "width": 900, "height": 700, "color": "(189, 195, 21)"}, {"x": 475, "y": 266, "width": 900, "height": 700, "color": "(181, 176, 47)"}, {"x": 521, "y": 308, "width": 900, "height": 700, "color": "(220, 225, 26)"}, {"x": 585, "y": 341, "width": 900, "height": 700, "color": "(202, 212, 21)"}, {"x": 486, "y": 334, "width": 900, "height": 700, "color": "(95, 113, 0)"}, {"x": 487, "y": 375, "width": 900, "height": 700, "color": "(128, 138, 22)"}, {"x": 531, "y": 381, "width": 900, "height": 700, "color": "(169, 145, 43)"}, {"x": 520, "y": 344, "width": 900, "height": 700, "color": "(253, 253, 66)"}, {"x": 409, "y": 380, "width": 900, "height": 700, "color": "(166, 162, 70)"}]} -------------------------------------------------------------------------------- /threads/base.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | import threading 5 | import time 6 | import gi 7 | gi.require_version('Gtk', '3.0') 8 | from gi.repository import GObject 9 | from lib.shared import LogType, DebugLevel 10 | 11 | ''' 12 | TimerThread is a quick implementation of thread class with a timer (not really powerful, but it do the job) 13 | ''' 14 | class TimerThread(threading.Thread): 15 | 16 | def __init__(self): 17 | threading.Thread.__init__(self) 18 | self.start_time = 0 19 | self.elapsed_time = 0 20 | 21 | def start_timer(self): 22 | self.elapsed_time = 0 23 | self.start_time = time.time() 24 | 25 | def pause_timer(self): 26 | if self.start_time > 0: # if timer started 27 | self.elapsed_time += time.time() - self.start_time # save elapsed time 28 | self.start_time = 0 # pause timer 29 | 30 | def resume_timer(self): 31 | if self.start_time == 0: # if timer paused or even not started 32 | self.start_time = time.time() # restart timer 33 | 34 | def stop_timer(self): 35 | self.pause_timer() 36 | 37 | def get_elapsed_time(self): 38 | if self.start_time > 0: # if timer started 39 | self.elapsed_time += time.time() - self.start_time # save elapsed time 40 | self.start_time = time.time() # restart timer 41 | 42 | return time.strftime('%H:%M:%S', time.gmtime(self.elapsed_time)) 43 | 44 | class PausableThread(TimerThread): 45 | 46 | def __init__(self, parent, game_location): 47 | TimerThread.__init__(self) 48 | self.parent = parent 49 | self.game_location = game_location 50 | self.pause_event = threading.Event() 51 | self.pause_event.set() 52 | self.suspend = False 53 | 54 | def slow_down(self): 55 | time.sleep(0.1) # reduce thread speed 56 | 57 | def log(self, text, type=LogType.Normal): 58 | GObject.idle_add(self.parent.log, text, type) 59 | self.slow_down() 60 | 61 | def debug(self, text, level=DebugLevel.Normal): 62 | GObject.idle_add(self.parent.debug, text, level) 63 | self.slow_down() 64 | 65 | def reset(self): 66 | GObject.idle_add(self.parent.reset_buttons) 67 | 68 | def pause(self): 69 | self._pause() 70 | GObject.idle_add(self.parent.set_buttons_to_paused) 71 | 72 | def _pause(self): 73 | self.pause_timer() 74 | self.pause_event.clear() 75 | self.debug('Bot thread paused', DebugLevel.Low) 76 | 77 | def resume(self, game_location): 78 | self.game_location = game_location 79 | self.resume_timer() 80 | self.pause_event.set() 81 | self.debug('Bot thread resumed', DebugLevel.Low) 82 | 83 | def stop(self): 84 | self.stop_timer() 85 | self.suspend = True 86 | self.pause_event.set() # ensure that thread is resumed (if ever paused) 87 | #self.join() # wait for thread to exit 88 | self.debug('Bot thread stopped', DebugLevel.Low) 89 | -------------------------------------------------------------------------------- /lib/settings.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | import json 5 | from .tools import read_file, save_text_to_file, get_full_path 6 | from .shared import DebugLevel, GameVersion 7 | 8 | def get_filename(): 9 | return get_full_path('settings.json') 10 | 11 | def load_defaults(): 12 | settings = { 13 | 'Debug': { 14 | 'Enabled': True, 15 | 'Level': DebugLevel.Low 16 | }, 17 | 'Shortcuts': { 18 | 'Start': None, 19 | 'Pause': None, 20 | 'Stop': None, 21 | 'Minimize': 'Alt_L', 22 | 'Focus Game': 'Ctrl+g', 23 | 'Take Game Screenshot': 'Ctrl+Print' 24 | }, 25 | 'Game': { 26 | 'Version': GameVersion.Two, 27 | 'UseCustomWindowDecorationHeight': False, 28 | 'WindowDecorationHeight': 36, 29 | 'KeepOpen': True 30 | }, 31 | 'Account': { 32 | 'ExitGame': False 33 | }, 34 | 'State': { 35 | 'EnablePodBar': True, 36 | 'EnableMiniMap': True 37 | }, 38 | 'Farming': { 39 | 'SaveDragodindesImages': False, 40 | 'CheckResourcesColor': True, 41 | 'AutoClosePopups': True, 42 | 'CollectionTime': 4, 43 | 'FirstResourceAdditionalCollectionTime': 5 44 | }, 45 | 'EnableShortcuts': False 46 | } 47 | return settings 48 | 49 | def load(): 50 | text = read_file(get_filename()) 51 | defaults = load_defaults() 52 | if text: 53 | # load settings 54 | settings = json.loads(text) 55 | # check if all settings are there 56 | for key in defaults: 57 | # check keys 58 | if not key in settings: 59 | settings[key] = defaults[key] 60 | # check subkeys 61 | elif isinstance(defaults[key], dict): 62 | for subkey in defaults[key]: 63 | if not subkey in settings[key]: 64 | settings[key][subkey] = defaults[key][subkey] 65 | return settings 66 | else: 67 | return defaults 68 | 69 | def save(settings): 70 | text = json.dumps(settings) 71 | save_text_to_file(text, get_filename()) 72 | 73 | def update_and_save(settings, key, value, subkey=None): 74 | if subkey is not None: 75 | settings[key][subkey] = value 76 | else: 77 | settings[key] = value 78 | save(settings) 79 | 80 | def get(settings, key, subkey=None): 81 | if key in settings: 82 | if subkey is not None: 83 | if subkey in settings[key]: 84 | return settings[key][subkey] 85 | else: 86 | defaults = load_defaults() 87 | if key in defaults and subkey in defaults[key]: 88 | return defaults[key][subkey] 89 | else: 90 | return None 91 | else: 92 | return settings[key] 93 | else: 94 | defaults = load_defaults() 95 | if key in defaults: 96 | if subkey is not None: 97 | if subkey in defaults[key]: 98 | return defaults[key][subkey] 99 | else: 100 | return None 101 | else: 102 | return defaults[key] 103 | else: 104 | return None 105 | -------------------------------------------------------------------------------- /threads/travel.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | from lib import data, tools, parser 5 | from .game import GameThread 6 | 7 | class TravelThread(GameThread): 8 | 9 | def __init__(self, parent, game_location): 10 | GameThread.__init__(self, parent, game_location) 11 | 12 | def move(self, direction): 13 | # get coordinates 14 | coordinates = parser.parse_data(data.Movements, direction) 15 | if coordinates: 16 | # click 17 | self.click(coordinates) 18 | # wait for map to change 19 | self.wait_for_map_change() 20 | 21 | def wait_for_map_change(self, timeout=30, tolerance=2.5, screen=None, load_time=3): 22 | # wait for map to change 23 | self.debug('Waiting for map to change') 24 | if self.monitor_game_screen(timeout=timeout, tolerance=tolerance, screen=screen): 25 | # wait for map to load 26 | self.debug('Waiting for map to load (%d sec)' % load_time) 27 | self.sleep(load_time) 28 | 29 | def use_zaap(self, zaap_from, zaap_to): 30 | # get coordinates 31 | zaap_from_coordinates = parser.parse_data(data.Zaap['From'], zaap_from) 32 | zaap_to_coordinates = parser.parse_data(data.Zaap['To'], zaap_to) 33 | if zaap_from_coordinates and zaap_to_coordinates: 34 | # if a keyboard shortcut is set (like for Havenbag) 35 | if 'keyboardShortcut' in zaap_from_coordinates: 36 | # press key 37 | self.press_key(zaap_from_coordinates['keyboardShortcut']) 38 | # wait for map to change 39 | self.wait_for_map_change() 40 | # check for pause or suspend 41 | self.pause_event.wait() 42 | if self.suspend: return 43 | # click on zaap from 44 | screen = tools.screen_game(self.game_location) 45 | self.click(zaap_from_coordinates) 46 | # wait for zaap list to show 47 | self.debug('Waiting for zaap list to show') 48 | self.monitor_game_screen(tolerance=2.5, screen=screen) 49 | # check for pause or suspend 50 | self.pause_event.wait() 51 | if self.suspend: return 52 | # if we need to scroll zaap list 53 | if 'scroll' in zaap_to_coordinates: 54 | # scroll 55 | self.scroll(zaap_to_coordinates['scroll']) 56 | # check for pause or suspend 57 | self.pause_event.wait() 58 | if self.suspend: return 59 | # double click on zaap to 60 | screen = tools.screen_game(self.game_location) 61 | self.double_click(zaap_to_coordinates) 62 | # wait for map to change 63 | self.wait_for_map_change(screen=screen) 64 | 65 | def use_zaapi(self, zaapi_from, zaapi_to): 66 | # get coordinates 67 | zaapi_from_coordinates = parser.parse_data(data.Zaapi['From'], zaapi_from) 68 | zaapi_to_coordinates = parser.parse_data(data.Zaapi['To'], zaapi_to) 69 | if zaapi_from_coordinates and zaapi_to_coordinates: 70 | # click on zaapi from 71 | screen = tools.screen_game(self.game_location) 72 | self.click(zaapi_from_coordinates) 73 | # wait for zaapi list to show 74 | self.debug('Waiting for zaapi list to show') 75 | self.monitor_game_screen(tolerance=2.5, screen=screen) 76 | # check for pause or suspend 77 | self.pause_event.wait() 78 | if self.suspend: return 79 | # choose tab/location from zaapi list 80 | self.click(zaapi_to_coordinates['location']) 81 | self.sleep(2) 82 | # check for pause or suspend 83 | self.pause_event.wait() 84 | if self.suspend: return 85 | # if we need to scroll zaapi list 86 | if 'scroll' in zaapi_to_coordinates: 87 | # scroll 88 | self.scroll(zaapi_to_coordinates['scroll']) 89 | # check for pause or suspend 90 | self.pause_event.wait() 91 | if self.suspend: return 92 | # double click on zaapi to 93 | screen = tools.screen_game(self.game_location) 94 | self.double_click(zaapi_to_coordinates) 95 | # wait for map to change 96 | self.wait_for_map_change(screen=screen) 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # logo Dindo Bot 2 | 3 | [![Python](https://img.shields.io/badge/python%20%3E%3D-3.0-blue.svg)](https://www.python.org/) 4 | [![GTK](https://img.shields.io/badge/gtk-3.0-brightgreen.svg)](https://www.gtk.org/) 5 | [![OS](https://img.shields.io/badge/os-Linux-orange.svg)](https://www.ubuntu.com/download/desktop) 6 | 7 | Farming bot for Dofus game written in python & [GTK](https://www.gtk.org/). 8 | 9 | > :construction: version 3 is work in progress, you can test it or contribute on the [v3-alpha](https://github.com/AXeL-dev/Dindo-Bot/tree/v3-alpha) branch. 10 | 11 | [![screenshot](screenshot.png)](http://www.youtube.com/watch?v=1Qh_eNLuTYo "Watch Dindo-bot in action") 12 | 13 | > What's a bot? 14 | 15 | A bot is a software that emulates a real game client in order to automate some tasks. Dindo is a **pixel** bot, which means that it uses screen pixels (+ your mouse & keyboard) to emulate actions & keep tracking the progress in game. 16 | 17 | :warning: Better know that this bot is only available on [Linux](https://www.wikipedia.org/wiki/Linux) for now. 18 | 19 | ## Features 20 | 21 | - **Easy control**: play, pause/resume or stop the bot like if you were using your favorite music player. 22 | - **Smart bot**: Dindo knows when your connection turns off and will wait for it to get back before proceeding or will automatically pause itself if it takes a long time. 23 | - **Multi accounts management**: you don't have to worry about switching between your accounts, Dindo can handle that for you :wink:. 24 | - **Integrated Path & Maps builder**: easily create your own custom paths & farming maps. 25 | - **Keyboard shortcuts**. 26 | 27 | ## Use Cases 28 | 29 | - Auto-connect to your Dofus account(s). 30 | - Move around the map & save time for long dungeon paths, [paths](paths) pull requests are welcome :pray:. 31 | - Farming & jobs (:construction: this part still needs to be improved, also, the bot cannot handle fights yet :warning:). 32 | - Automated actions like flood :speech_balloon:. 33 | 34 | ## Installation 35 | 36 | First, clone this repository using git or just [download](https://github.com/AXeL-dev/Dindo-Bot/archive/master.zip) & unzip it: 37 | 38 |
39 | git installation 40 | 41 | ```bash 42 | sudo apt install git 43 | ``` 44 |
45 | 46 | ```bash 47 | git clone https://github.com/AXeL-dev/Dindo-Bot.git 48 | ``` 49 | 50 | Then, open a terminal & launch the installation script as below: 51 | ```bash 52 | cd /path/to/bot 53 | chmod +x install.sh 54 | sudo ./install.sh 55 | ``` 56 | 57 | Once installed, you can run the bot from your app launcher or using the command below: 58 | ```bash 59 | dindo-bot 60 | ``` 61 | 62 | ## Tutorials 63 | 64 | - [Farming tutorial](https://www.youtube.com/watch?v=obGDT9_AXvk) 65 | 66 | ## To Know 67 | 68 | - You cannot use your computer for something else while Dindo is running. 69 | - Since the bot simulates normal human behavior, you have less chances to get spotted by the Anti-bot (less is not 0). 70 | > Tips: For more safety, try changing bot paths from time to time. 71 | - The main goal of this bot is to simplify repetitive tasks and reduce boredom during your gameplay. 72 | - We do not encourage multi-boting and do not support it anyway (it destroys the server economy :grimacing:). 73 | - Windows & Mac OS are not ~~yet~~ supported [#1](https://github.com/AXeL-dev/Dindo-Bot/issues/1) [#8](https://github.com/AXeL-dev/Dindo-Bot/issues/8). 74 | 75 | ## Version History 76 | 77 | - [v1.x](https://github.com/AXeL-dev/Dindo-Bot/tree/v1.x) 78 | 79 | ## Contributing 80 | 81 | Want to contribute? Check the [todo list](todo.md). You may also read the [contributing guidelines](https://github.com/AXeL-dev/contributing/blob/master/README.md). 82 | 83 | ## License 84 | 85 | Dindo-bot is licensed under the [MIT](LICENSE) license. 86 | -------------------------------------------------------------------------------- /lib/parser.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | from .convert import rgb2hex 5 | 6 | # Replace given string index by substitute 7 | def replace_at_index(string, index, substitute, length=1): 8 | return string[:index] + substitute + string[index+length:] 9 | 10 | # Replace all occurrences of needle in text with substitute only if needle is surrounded by starts_with & ends_with 11 | def replace_all_between(text, needle, substitute, starts_with, ends_with): 12 | # TODO: use regex 13 | result = text 14 | cursor_position = 0 15 | proceed = needle != substitute 16 | while proceed: # while True: 17 | #print('text: "%s", cursor: %d' % (text, cursor_position)) 18 | if len(text) == 0: 19 | break 20 | # try to find needle & his surroundings 21 | start = text.find(starts_with) 22 | needle_position = text.find(needle) 23 | end = text.find(ends_with) 24 | # if needle is surrounded 25 | if start != -1 and end != -1 and start < needle_position < end: 26 | result = replace_at_index(result, cursor_position+needle_position, substitute, len(needle)) 27 | cursor_position += end+1-len(needle)+len(substitute) 28 | text = text[end+1:] 29 | #print('replaced') 30 | # else if not surrounded 31 | elif needle_position != -1: 32 | cursor_position += needle_position+1 33 | text = text[needle_position+1:] 34 | #print('not surrounded') 35 | # else if not found 36 | else: 37 | #print('break') 38 | break 39 | return result 40 | 41 | # Parse instruction string, e.: 'Move(UP)', 'Enclos([-20,10])' 42 | def parse_instruction(line, separator=','): 43 | result = {} 44 | # extract name 45 | instruction = line.split('(') 46 | name = instruction[0] 47 | result['name'] = name 48 | # extract value/parameters 49 | if len(instruction) > 1: 50 | if instruction[1].endswith(')'): 51 | origin_value = instruction[1][:-1].strip() # [:-1] will remove the last character ')' 52 | else: 53 | origin_value = instruction[1].strip() 54 | if not origin_value: 55 | result['value'] = None 56 | else: 57 | value = replace_all_between(origin_value, ',', ';', '[', ']') # avoid splitting map position(s) 58 | parameters = value.split(separator) 59 | # if single parameter + without name, return it as instruction value 60 | if len(parameters) == 1 and '=' not in parameters[0]: 61 | result['value'] = replace_all_between(value, ';', ',', '[', ']') # value & parameters[0] are the same in this case 62 | else: 63 | # return parameter(s) name/index & their value(s) 64 | for i, parameter in enumerate(parameters): 65 | split_parameter = parameter.split('=') 66 | if len(split_parameter) > 1: 67 | key = split_parameter[0].strip() 68 | value = split_parameter[1] 69 | else: 70 | key = i 71 | value = split_parameter[0] 72 | result[key] = replace_all_between(value.strip(), ';', ',', '[', ']') 73 | else: 74 | result['value'] = None 75 | 76 | return result 77 | 78 | # Parse data dictionary, e.: {'x': 200, 'y': 10}, {'Zaap Bonta': {'x': 100, 'y': 50}} 79 | def parse_data(data, key, subkeys=[]): 80 | if key in data: 81 | if subkeys: 82 | count = len(subkeys) 83 | many = {} # dict to store subkeys data if there is more than 1 84 | for subkey in subkeys: 85 | if subkey in data[key]: 86 | if count == 1: 87 | return data[key][subkey] 88 | else: 89 | many[subkey] = data[key][subkey] 90 | else: 91 | if count == 1: 92 | return None 93 | else: 94 | many[subkey] = None 95 | return many 96 | else: 97 | return data[key] 98 | 99 | return None 100 | 101 | # Parse key string, e.: 'a', 'ctrl+c' 102 | def parse_key(key): 103 | result = [] 104 | 105 | keys = key.split('+') 106 | 107 | for part in keys: 108 | result.append(part.strip()) 109 | 110 | return result 111 | 112 | # Parse color string, e.: '(255, 255, 255)' 113 | def parse_color(color, as_hex=False): 114 | if type(color) is tuple: 115 | return rgb2hex(color) if as_hex else color 116 | # check if RGB 117 | elif color.startswith('(') and color.endswith(')'): 118 | values = color[1:-1].split(',') # [1:-1] will remove the first & last parentheses '(' ')' 119 | if len(values) == 3: 120 | rgb = (int(values[0]), int(values[1]), int(values[2])) 121 | return rgb2hex(rgb) if as_hex else rgb 122 | else: 123 | return None 124 | # check if HEX 125 | elif color.startswith('#') and len(color) in (4, 7): # '#xxx' or '#xxxxxx' 126 | return color 127 | else: 128 | return None 129 | -------------------------------------------------------------------------------- /threads/bot.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | from lib.shared import LogType, DebugLevel 5 | from lib import tools, parser 6 | from .job import JobThread 7 | 8 | class BotThread(JobThread): 9 | 10 | def __init__(self, parent, game_location, start_from_step, repeat_path, account_id, disconnect_after): 11 | JobThread.__init__(self, parent, game_location) 12 | self.start_from_step = start_from_step 13 | self.repeat_path = repeat_path 14 | self.account_id = account_id 15 | self.disconnect_after = disconnect_after 16 | self.exit_game = parent.settings['Account']['ExitGame'] 17 | 18 | def run(self): 19 | self.start_timer() 20 | self.debug('Bot thread started', DebugLevel.Low) 21 | 22 | # connect to account 23 | account_connected = False 24 | if self.account_id is not None: 25 | self.debug('Connect to account (account_id: %s)' % self.account_id) 26 | self.connect(self.account_id) 27 | account_connected = True 28 | # check for pause 29 | self.pause_event.wait() 30 | 31 | # get instructions & interpret them 32 | if not self.suspend: 33 | self.debug('Bot path: %s, repeat: %d' % (self.parent.bot_path, self.repeat_path)) 34 | if self.parent.bot_path: 35 | instructions = tools.read_file(self.parent.bot_path) 36 | repeat_count = 0 37 | while repeat_count < self.repeat_path: 38 | # check for pause or suspend 39 | self.pause_event.wait() 40 | if self.suspend: break 41 | # start interpretation 42 | self.interpret(instructions) 43 | repeat_count += 1 44 | 45 | # tell user that we have complete the path 46 | if not self.suspend: 47 | self.log('Bot path completed', LogType.Success) 48 | 49 | if not self.suspend: 50 | # disconnect account 51 | if account_connected and self.disconnect_after: 52 | self.debug('Disconnect account') 53 | self.disconnect(self.exit_game) 54 | # reset bot window buttons 55 | self.reset() 56 | 57 | self.debug('Bot thread ended, elapsed time: ' + self.get_elapsed_time(), DebugLevel.Low) 58 | 59 | def interpret(self, instructions, ignore_start_from_step=False): 60 | # split instructions 61 | if not isinstance(instructions, list): 62 | lines = instructions.splitlines() 63 | else: 64 | lines = instructions 65 | # ignore instructions before start step 66 | if not ignore_start_from_step and self.start_from_step > 1 and self.start_from_step <= len(lines): 67 | self.debug('Start from step: %d' % self.start_from_step) 68 | step = self.start_from_step - 1 69 | lines = lines[step:] 70 | 71 | game_screen = None 72 | for i, line in enumerate(lines, start=1): 73 | # check for pause or suspend 74 | self.pause_event.wait() 75 | if self.suspend: break 76 | 77 | # parse instruction 78 | self.debug('Instruction (%d): %s' % (i, line), DebugLevel.Low) 79 | instruction = parser.parse_instruction(line) 80 | self.debug('Parse result: ' + str(instruction), DebugLevel.High) 81 | 82 | # save game screen before those instructions 83 | if instruction['name'] in ['Click', 'PressKey']: 84 | game_screen = tools.screen_game(self.game_location) 85 | elif instruction['name'] != 'MonitorGameScreen': 86 | game_screen = None 87 | 88 | # begin interpretation 89 | if instruction['name'] == 'Move': 90 | self.move(instruction['value']) 91 | 92 | elif instruction['name'] == 'Enclos': 93 | self.check_enclos(instruction['location'], instruction['type']) 94 | 95 | elif instruction['name'] == 'Zaap': 96 | self.use_zaap(instruction['from'], instruction['to']) 97 | 98 | elif instruction['name'] == 'Zaapi': 99 | self.use_zaapi(instruction['from'], instruction['to']) 100 | 101 | elif instruction['name'] == 'Collect': 102 | self.collect(instruction['map'], instruction['store_path']) 103 | 104 | elif instruction['name'] == 'Click': 105 | coordinates = { 106 | 'x': int(instruction['x']), 107 | 'y': int(instruction['y']), 108 | 'width': int(instruction['width']), 109 | 'height': int(instruction['height']) 110 | } 111 | if instruction['twice'] == 'True': 112 | self.double_click(coordinates) 113 | else: 114 | self.click(coordinates) 115 | 116 | elif instruction['name'] == 'Scroll': 117 | times = int(instruction['times']) 118 | value = times if instruction['direction'] == 'up' else -times 119 | self.scroll(value) 120 | 121 | elif instruction['name'] == 'Pause': 122 | self.pause() 123 | 124 | elif instruction['name'] == 'MonitorGameScreen': 125 | self.monitor_game_screen(tolerance=2.5, screen=game_screen) 126 | 127 | elif instruction['name'] == 'Wait': 128 | duration = int(instruction['value']) 129 | self.sleep(duration) 130 | 131 | elif instruction['name'] == 'PressKey': 132 | self.press_key(instruction['value']) 133 | 134 | elif instruction['name'] == 'TypeText': 135 | self.type_text(instruction['value']) 136 | 137 | elif instruction['name'] == 'Connect': 138 | if instruction['account_id'].isdigit(): 139 | account_id = int(instruction['account_id']) 140 | else: 141 | account_id = instruction['account_id'] 142 | self.connect(account_id) 143 | 144 | elif instruction['name'] == 'Disconnect': 145 | self.disconnect(instruction['value']) 146 | 147 | else: 148 | self.debug('Unknown instruction', DebugLevel.Low) 149 | -------------------------------------------------------------------------------- /lib/imgcompare.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2016 Jonas Hahn 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from PIL import Image 24 | from PIL import ImageChops 25 | 26 | """ 27 | === imagecompare === 28 | 29 | This little tool compares two images using pillow's ImageChops and then converts the differnce to 30 | black/white and sums up all found differences by summing up the histogram values of the difference 31 | pixels. 32 | 33 | Taking the difference between a black and a white image of the same size as base a percentage value 34 | is calculated. 35 | 36 | Check the tests to see example diffs for different scenarios. Don't expect the diff of two jpg images be 37 | the same for the same images converted to png. Don't do interformat compares (e.g. JPG with PNG). 38 | 39 | Usage: 40 | 41 | == compare images == 42 | 43 | same = is_equal("image_a.jpg", "image_b.jpg") 44 | 45 | # use the tolerance parameter to allow a certain diff pass as same 46 | same = is_equal("image_a.jpg", "image_b.jpg", tolerance=2.5) 47 | 48 | == get the diff percentage == 49 | 50 | percentage = image_diff_percent("image_a.jpg", "image_b.jpg") 51 | 52 | # or work directly with pillow image instances 53 | image_a = Image.open("image_a.jpg") 54 | image_b = Image.open("image_b.jpg") 55 | percentage = image_diff_percent(image_a, image_b) 56 | 57 | """ 58 | 59 | 60 | class ImageCompareException(Exception): 61 | """ 62 | Custom Exception class for imagecompare's exceptions. 63 | """ 64 | pass 65 | 66 | 67 | def pixel_diff(image_a, image_b): 68 | """ 69 | Calculates a black/white image containing all differences between the two input images. 70 | 71 | :param image_a: input image A 72 | :param image_b: input image B 73 | :return: a black/white image containing the differences between A and B 74 | """ 75 | 76 | if image_a.size != image_b.size: 77 | raise ImageCompareException( 78 | "different image sizes, can only compare same size images: A=" + str(image_a.size) + " B=" + str( 79 | image_b.size)) 80 | 81 | if image_a.mode != image_b.mode: 82 | raise ImageCompareException( 83 | "different image mode, can only compare same mode images: A=" + str(image_a.mode) + " B=" + str( 84 | image_b.mode)) 85 | 86 | diff = ImageChops.difference(image_a, image_b) 87 | diff = diff.convert('L') 88 | 89 | return diff 90 | 91 | 92 | def total_histogram_diff(pixel_diff): 93 | """ 94 | Sums up all histogram values of an image. When used with the black/white pixel-diff image 95 | this gives the difference "score" of an image. 96 | 97 | :param pixel_diff: the black/white image containing all differences (output of imagecompare.pixel_diff function) 98 | :return: the total "score" of histogram values (histogram values of found differences) 99 | """ 100 | return sum(i * n for i, n in enumerate(pixel_diff.histogram())) 101 | 102 | 103 | def image_diff(image_a, image_b): 104 | """ 105 | Calculates the total difference "score" of two images. (see imagecompare.total_histogram_diff). 106 | 107 | :param image_a: input image A 108 | :param image_b: input image A 109 | :return: the total difference "score" between two images 110 | """ 111 | histogram_diff = total_histogram_diff(pixel_diff(image_a, image_b)) 112 | 113 | return histogram_diff 114 | 115 | 116 | def is_equal(image_a, image_b, tolerance=0.0): 117 | """ 118 | Compares two image for equalness. By specifying a tolerance a certain diff can 119 | be allowed to pass as True. 120 | 121 | :param image_a: input image A 122 | :param image_b: input image B 123 | :param tolerance: allow up to (including) a certain percentage of diff pass as True 124 | :return: True if the images are the same, false if they differ 125 | """ 126 | return image_diff_percent(image_a, image_b) <= tolerance 127 | 128 | 129 | def image_diff_percent(image_a, image_b): 130 | """ 131 | Calculate the difference between two images in percent. 132 | 133 | :param image_a: input image A 134 | :param image_b: input image B 135 | :return: the difference between the images A and B as percentage 136 | """ 137 | 138 | # if paths instead of image instances where passed in 139 | # load the images 140 | if isinstance(image_a, str): 141 | image_a = Image.open(image_a) 142 | 143 | if isinstance(image_b, str): 144 | image_b = Image.open(image_b) 145 | 146 | # first determine difference of input images 147 | input_images_histogram_diff = image_diff(image_a, image_b) 148 | 149 | # to get the worst possible difference use a black and a white image 150 | # of the same size and diff them 151 | 152 | black_reference_image = Image.new('RGB', image_a.size, (0, 0, 0)) 153 | white_reference_image = Image.new('RGB', image_a.size, (255, 255, 255)) 154 | 155 | worst_bw_diff = image_diff(black_reference_image, white_reference_image) 156 | 157 | percentage_histogram_diff = (input_images_histogram_diff / float(worst_bw_diff)) * 100 158 | 159 | return percentage_histogram_diff 160 | -------------------------------------------------------------------------------- /lib/data.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Dindo Bot 3 | # Copyright (c) 2018 - 2019 AXeL 4 | 5 | # Movements 6 | Movements = { 7 | 'UP': {'x': 266, 'y': 4, 'width': 566, 'height': 456}, 8 | 'LEFT': {'x': 5, 'y': 224, 'width': 566, 'height': 456}, 9 | 'RIGHT': {'x': 561, 'y': 225, 'width': 566, 'height': 456}, 10 | 'DOWN': {'x': 263, 'y': 397, 'width': 566, 'height': 456}, 11 | # TODO: rename 'width' & 'height' to 'windowSize' (this may impact many parts of code) 12 | } 13 | 14 | # Locations 15 | Locations = { 16 | 'Workshops Menu': {'x': 187, 'y': 75, 'width': 566, 'height': 456}, 17 | 'SH Menu': {'x': 261, 'y': 75, 'width': 566, 'height': 456}, 18 | 'Various Menu': {'x': 342, 'y': 75, 'width': 566, 'height': 456}, 19 | 'Select From Enclos': {'x': 105, 'y': 297, 'width': 566, 'height': 456}, 20 | 'Select From Inventory': {'x': 450, 'y': 165, 'width': 566, 'height': 456}, 21 | 'Disconnect Button': {'x': 282, 'y': 218, 'width': 566, 'height': 456}, 22 | 'Exit Button': {'x': 282, 'y': 236, 'width': 566, 'height': 456}, 23 | 'Login Input': {'x': 270, 'y': 146, 'width': 566, 'height': 456}, 24 | 'Password Input': {'x': 270, 'y': 174, 'width': 566, 'height': 456}, 25 | # TODO: rename 'width' & 'height' to 'windowSize' (this may impact many parts of code) 26 | } 27 | 28 | # Boxes 29 | Boxes = { 30 | 'Dragodinde Card': {'x': 207, 'y': 42, 'width': 159, 'height': 320, 'windowSize': (566, 456)}, 31 | 'Dragodinde Energy': {'x': 75, 'y': 136, 'width': 69, 'height': 1, 'windowSize': (566, 456)}, 32 | #'Dragodinde Experience': {'x': 75, 'y': 148, 'width': 69, 'height': 1, 'windowSize': (566, 456)}, 33 | #'Dragodinde Tiredness': {'x': 75, 'y': 159, 'width': 69, 'height': 1, 'windowSize': (566, 456)}, 34 | 'Dragodinde Amour': {'x': 75, 'y': 218, 'width': 69, 'height': 1, 'windowSize': (566, 456)}, 35 | 'Dragodinde Maturity': {'x': 75, 'y': 229, 'width': 69, 'height': 1, 'windowSize': (566, 456)}, 36 | 'Dragodinde Endurance': {'x': 75, 'y': 240, 'width': 69, 'height': 1, 'windowSize': (566, 456)}, 37 | 'Dragodinde Serenity': {'x': 26, 'y': 274, 'width': 106, 'height': 1, 'windowSize': (566, 456)}, 38 | 'Enclos First Place': {'x': 28, 'y': 295, 'width': 167, 'height': 1, 'windowSize': (566, 456)}, 39 | 'Inventory First Place': {'x': 377, 'y': 165, 'width': 168, 'height': 1, 'windowSize': (566, 456)}, 40 | 'Play Button': {'x': 341, 'y': 362, 'width': 96, 'height': 1, 'windowSize': (566, 456)}, 41 | 'Login Button': {'x': 400, 'y': 385, 'width': 96, 'height': 1, 'windowSize': (900, 700)}, # not accurate 42 | 'PodBar': {'x': 658, 'y': 564, 'width': 81, 'height': 1, 'windowSize': (900, 700)}, 43 | 'Fight Button': {'x': 706, 'y': 640, 'width': 81, 'height': 1, 'windowSize': (900, 700)}, 44 | 'Job Level Up Popup': {'x': 249, 'y': 397, 'width': 11, 'height': 1, 'windowSize': (900, 713)}, 45 | # TODO: update all boxes coordinates to (900, 700) window size 46 | } 47 | 48 | # DragodindeSerenity 49 | class DragodindeSerenity: 50 | Negative, Medium, Positive = ('Negative', 'Medium', 'Positive') 51 | MaxMedium = 57.55 52 | 53 | # DragodindeEnergy 54 | class DragodindeEnergy: 55 | Max = 98.55 56 | 57 | # DragodindeStats 58 | class DragodindeStats: 59 | Empty, InProgress, Full = ('Empty', 'In Progress', 'Full') 60 | 61 | # Colors 62 | Colors = { 63 | 'Full': (204, 246, 0), 64 | 'In Progress': (255, 106, 61), 65 | #'Experience': (108, 240, 229), 66 | #'Tiredness': (252, 200, 0), 67 | 'Empty Enclos': (56, 56, 49), 68 | 'Selected Row': (83, 83, 77), 69 | 'Empty Inventory': (53, 53, 45), 70 | 'Play Button': (215, 247, 0), 71 | 'Login Button': (214, 246, 0), 72 | 'Empty PodBar': (67, 70, 68), 73 | 'Fight Button': (216, 244, 0), 74 | 'Job Level Up Popup': (229, 249, 0), 75 | } 76 | 77 | # Keyboard shortcuts 78 | KeyboardShortcuts = { 79 | 'Havenbag': 'h', 80 | 'Inventory': 'i', 81 | 'Store': '&', 82 | 'Equip': '"', 83 | 'Elevate': '2', 84 | 'Exchange': '\'', 85 | 'Up': 'up', 86 | 'Down': 'down', 87 | 'Left': 'left', 88 | 'Right': 'right', 89 | 'Backspace': 'backspace', 90 | 'Enter': 'enter', 91 | 'Tab': 'tab', 92 | 'Esc': 'esc' 93 | #'Copy': 'ctrl+c', 94 | #'Cut': 'ctrl+x', 95 | #'Paste': 'ctrl+v', 96 | } 97 | 98 | # Enclos 99 | EnclosType = [ 100 | 'Amour', 101 | 'Energy', 102 | 'Endurance', 103 | 'Maturity', 104 | 'NegativeSerenity', 105 | 'PositiveSerenity', 106 | ] 107 | 108 | Enclos = { 109 | # Bonta 110 | '[-37,-56]': {'x': 354, 'y': 144, 'width': 566, 'height': 456}, 111 | # Brakmar 112 | '[-32,37]': {'x': 234, 'y': 202, 'width': 566, 'height': 456}, 113 | # TODO: rename 'width' & 'height' to 'windowSize' (this may impact many parts of code) 114 | } 115 | 116 | # Zaap 117 | Zaap = { 118 | 'From': { 119 | 'Havenbag': {'x': 110, 'y': 162, 'width': 566, 'height': 456, 'keyboardShortcut': KeyboardShortcuts['Havenbag']}, 120 | 'Bonta': {'x': 248, 'y': 140, 'width': 566, 'height': 456}, 121 | 'Brakmar': {'x': 305, 'y': 106, 'width': 566, 'height': 456}, 122 | }, 123 | 'To': { 124 | 'Bonta': {'x': 177, 'y': 274, 'width': 566, 'height': 456, 'scroll': -1}, 125 | 'Brakmar': {'x': 182, 'y': 295, 'width': 566, 'height': 456, 'scroll': -1}, 126 | } 127 | # TODO: rename 'width' & 'height' to 'windowSize' (this may impact many parts of code) 128 | } 129 | 130 | # Zaapi 131 | Zaapi = { 132 | 'From': { 133 | # Bonta 134 | 'Zaap Bonta': {'x': 58, 'y': 67, 'width': 566, 'height': 456}, 135 | 'Bank Bonta': {'x': 450, 'y': 242, 'width': 566, 'height': 456}, 136 | 'Animal SH Bonta': {'x': 178, 'y': 48, 'width': 566, 'height': 456}, 137 | # Brakmar 138 | 'Zaap Brakmar': {'x': 435, 'y': 161, 'width': 566, 'height': 456}, 139 | 'Bank Brakmar': {'x': 154, 'y': 45, 'width': 566, 'height': 456}, 140 | 'Animal SH Brakmar': {'x': 434, 'y': 62, 'width': 566, 'height': 456}, 141 | }, 142 | 'To': { 143 | 'Zaap': {'x': 176, 'y': 298, 'width': 566, 'height': 456, 'scroll': -2, 'location': Locations['Various Menu']}, 144 | 'Animal SH': {'x': 203, 'y': 175, 'width': 566, 'height': 456, 'location': Locations['SH Menu']}, 145 | 'Bank': {'x': 183, 'y': 153, 'width': 566, 'height': 456, 'location': Locations['Various Menu']}, 146 | } 147 | # TODO: rename 'width' & 'height' to 'windowSize' (this may impact many parts of code) 148 | } 149 | 150 | # BankPath 151 | BankPath = { 152 | # Bonta 153 | 'Bank Bonta': [ 154 | 'Zaap(from=Havenbag,to=Bonta)', 155 | 'Zaapi(from=Zaap Bonta,to=Bank)', 156 | # TODO: complete this path... 157 | ], 158 | # Brakmar 159 | 'Bank Brakmar': [ 160 | 'Zaap(from=Havenbag,to=Brakmar)', 161 | 'Zaapi(from=Zaap Brakmar,to=Bank)', 162 | # TODO: complete this path... 163 | ] 164 | } 165 | -------------------------------------------------------------------------------- /threads/job.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | import gi 5 | gi.require_version('Gtk', '3.0') 6 | from gi.repository import GObject 7 | from gui.custom import MiniMap 8 | from lib.shared import LogType, DebugLevel, GameVersion 9 | from lib import maps, data, tools, parser 10 | from .farming import FarmingThread 11 | 12 | class JobThread(FarmingThread): 13 | 14 | def __init__(self, parent, game_location): 15 | FarmingThread.__init__(self, parent, game_location) 16 | self.podbar_enabled = parent.settings['State']['EnablePodBar'] 17 | self.minimap_enabled = parent.settings['State']['EnableMiniMap'] 18 | self.check_resources_color = parent.settings['Farming']['CheckResourcesColor'] 19 | self.auto_close_popups = parent.settings['Farming']['AutoClosePopups'] 20 | self.collection_time = parent.settings['Farming']['CollectionTime'] 21 | self.first_resource_additional_collection_time = parent.settings['Farming']['FirstResourceAdditionalCollectionTime'] 22 | self.game_version = parent.settings['Game']['Version'] 23 | 24 | def collect(self, map_name, store_path): 25 | map_data = parser.parse_data(maps.load(), map_name) 26 | if map_data: 27 | # show resources on minimap 28 | self.update_minimap(map_data, 'Resource', MiniMap.point_colors['Resource']) 29 | # collect resources 30 | is_first_resource = True 31 | for resource in map_data: 32 | # check for pause or suspend 33 | self.pause_event.wait() 34 | if self.suspend: return 35 | # check resource color 36 | if not self.check_resource_color(resource): 37 | # go to next resource 38 | continue 39 | # screen game 40 | screen = tools.screen_game(self.game_location) 41 | # click on resource 42 | self.debug("Collecting resource {'x': %d, 'y': %d, 'color': %s}" % (resource['x'], resource['y'], resource['color'])) 43 | self.click(resource) 44 | if self.game_version == GameVersion.Retro: 45 | # re-click to validate 46 | self.click({'x': resource['x'] + 30, 'y': resource['y'] + 40, 'width': resource['width'], 'height': resource['height']}) 47 | # wait before collecting next one 48 | if is_first_resource: 49 | is_first_resource = False 50 | self.sleep(self.first_resource_additional_collection_time) # wait more for 1st resource 51 | self.sleep(self.collection_time) 52 | # remove current resource from minimap (index = 0) 53 | self.remove_from_minimap(0) 54 | # check for pause or suspend 55 | self.pause_event.wait() 56 | if self.suspend: return 57 | # check for screen change 58 | self.debug('Checking for screen change') 59 | if self.monitor_game_screen(tolerance=2.5, screen=screen, timeout=1, wait_after_timeout=False): 60 | # check for fight 61 | if self.game_version != GameVersion.Retro and self.wait_for_box_appear(box_name='Fight Button', timeout=1): 62 | self.pause() 63 | self.log('Fight detected! human help wanted..', LogType.Error) 64 | elif self.auto_close_popups: 65 | # it should be a popup (level up, ...) 66 | self.debug('Closing popup') 67 | screen = tools.screen_game(self.game_location) 68 | if self.game_version == GameVersion.Retro: 69 | self.press_key(data.KeyboardShortcuts['Enter']) 70 | elif self.wait_for_box_appear(box_name='Job Level Up Popup', timeout=1): 71 | location = self.get_box_location('Job Level Up Popup') 72 | self.click(location) 73 | else: 74 | self.press_key(data.KeyboardShortcuts['Esc']) 75 | # wait for popup to close 76 | self.monitor_game_screen(tolerance=2.5, screen=screen) 77 | # check for pause or suspend 78 | self.pause_event.wait() 79 | if self.suspend: return 80 | # get pod 81 | if self.game_version != GameVersion.Retro and self.get_pod() >= 99: 82 | # pod is full, go to store 83 | if store_path != 'None': 84 | self.go_to_store(store_path) 85 | else: 86 | self.pause() 87 | self.log('Bot is full pod', LogType.Error) 88 | 89 | def check_resource_color(self, resource): 90 | # check pixel color 91 | if self.check_resources_color: 92 | game_x, game_y, game_width, game_height = self.game_location 93 | x, y = tools.adjust_click_position(resource['x'], resource['y'], resource['width'], resource['height'], game_x, game_y, game_width, game_height) 94 | color = tools.get_pixel_color(x, y) 95 | resource['color'] = parser.parse_color(resource['color']) 96 | if resource['color'] is not None and not tools.color_matches(color, resource['color'], tolerance=10): 97 | self.debug("Ignoring non-matching resource {'x': %d, 'y': %d, 'color': %s} on pixel {'x': %d, 'y': %d, 'color': %s}" % (resource['x'], resource['y'], resource['color'], x, y, color)) 98 | # remove current resource from minimap (index = 0) 99 | self.remove_from_minimap(0) 100 | return False 101 | 102 | return True 103 | 104 | def go_to_store(self, store_path): 105 | self.debug('Go to store (path: %s)' % store_path) 106 | if store_path in data.BankPath: 107 | instructions = data.BankPath[store_path] 108 | else: 109 | instructions = tools.read_file(tools.get_full_path(store_path)) 110 | if instructions: 111 | self.interpret(instructions, ignore_start_from_step=True) 112 | else: 113 | self.pause() 114 | self.debug('Could not interpret store path') 115 | self.log('Bot is maybe full pod', LogType.Error) 116 | 117 | def get_pod(self): 118 | # open inventory 119 | self.debug('Opening inventory') 120 | screen = tools.screen_game(self.game_location) 121 | self.press_key(data.KeyboardShortcuts['Inventory']) 122 | # wait for inventory to open 123 | self.monitor_game_screen(tolerance=2.5, screen=screen) 124 | # check for pause or suspend 125 | self.pause_event.wait() 126 | if self.suspend: return 127 | # get podbar color & percentage 128 | location = self.get_box_location('PodBar') 129 | screen = tools.screen_game(location) 130 | color, percentage = tools.get_dominant_color(screen) 131 | # close inventory 132 | self.debug('Closing inventory') 133 | screen = tools.screen_game(self.game_location) 134 | self.press_key(data.KeyboardShortcuts['Inventory']) 135 | # wait for inventory to close 136 | self.monitor_game_screen(tolerance=2.5, screen=screen) 137 | # check for pause or suspend 138 | self.pause_event.wait() 139 | if self.suspend: return 140 | # check if podbar is empty 141 | if tools.color_matches(color, data.Colors['Empty PodBar'], tolerance=10): 142 | percentage = 0 143 | # update pod bar 144 | self.set_pod(percentage) 145 | return percentage 146 | 147 | def set_pod(self, percentage): 148 | if self.podbar_enabled: 149 | self.debug('Update PodBar (percentage: {}%)'.format(percentage)) 150 | # set podbar value 151 | GObject.idle_add(self.parent.podbar.set_fraction, percentage / 100.0) 152 | 153 | def update_minimap(self, points, points_name=None, points_color=None): 154 | if self.minimap_enabled: 155 | self.debug('Update MiniMap') 156 | # clear minimap 157 | GObject.idle_add(self.parent.minimap.clear) 158 | # update minimap 159 | GObject.idle_add(self.parent.minimap.add_points, points, points_name, points_color) 160 | 161 | def remove_from_minimap(self, index): 162 | if self.minimap_enabled: 163 | self.debug('Remove point from MiniMap (index: %d)' % index) 164 | GObject.idle_add(self.parent.minimap.remove_point, index) 165 | -------------------------------------------------------------------------------- /threads/game.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | import time 5 | import gi 6 | gi.require_version('Gtk', '3.0') 7 | from gi.repository import GObject 8 | from lib.shared import LogType, DebugLevel 9 | from lib import data, tools, imgcompare, accounts 10 | from .base import PausableThread 11 | 12 | class GameThread(PausableThread): 13 | 14 | def __init__(self, parent, game_location): 15 | PausableThread.__init__(self, parent, game_location) 16 | 17 | def connect(self, account_id): 18 | # Login button detection 19 | self.debug('Detecting Login button') 20 | if self.wait_for_box_appear(box_name='Login Button', timeout=10): 21 | # get account 22 | account = accounts.get(account_id) 23 | if account: 24 | # slow down 25 | self.sleep(1) 26 | # (1) type login 27 | self.double_click(data.Locations['Login Input']) 28 | self.press_key(data.KeyboardShortcuts['Backspace']) # clean 29 | self.type_text(account['login']) 30 | # check for pause or suspend 31 | self.pause_event.wait() 32 | if self.suspend: return 33 | # (2) type password 34 | self.double_click(data.Locations['Password Input']) 35 | self.press_key(data.KeyboardShortcuts['Backspace']) # clean 36 | self.type_text(account['pwd']) 37 | # check for pause or suspend 38 | self.pause_event.wait() 39 | if self.suspend: return 40 | # (3) hit enter 41 | self.debug('Hit Play') 42 | self.press_key(data.KeyboardShortcuts['Enter']) 43 | # wait for Play button to appear 44 | self.debug('Waiting for Play button to appear') 45 | if self.wait_for_box_appear(box_name='Play Button'): 46 | # (4) hit enter again 47 | self.debug('Hit Play') 48 | self.press_key(data.KeyboardShortcuts['Enter']) 49 | # wait for load to start 50 | self.debug('Waiting for load to start') 51 | self.sleep(2) 52 | # check for pause or suspend 53 | self.pause_event.wait() 54 | if self.suspend: return 55 | # wait for screen to change 56 | self.wait_for_screen_change(load_time=5) 57 | elif not self.suspend: 58 | self.pause() 59 | self.log('Unable to connect to account', LogType.Error) 60 | else: 61 | self.pause() 62 | self.log('Account not found', LogType.Error) 63 | elif not self.suspend: 64 | self.pause() 65 | self.log('Login button detection failed', LogType.Error) 66 | 67 | def disconnect(self, exit=False): 68 | # (1) press 'Esc' key 69 | self.press_key(data.KeyboardShortcuts['Esc']) 70 | # wait for menu to show 71 | self.sleep(1) 72 | # check for pause or suspend 73 | self.pause_event.wait() 74 | if self.suspend: return 75 | # (2) click on disconnect/exit 76 | button = 'Exit Button' if exit == 'True' else 'Disconnect Button' 77 | self.click(data.Locations[button]) 78 | # wait for confirmation to show 79 | self.sleep(1) 80 | # check for pause or suspend 81 | self.pause_event.wait() 82 | if self.suspend: return 83 | # (3) confirm disconnect/exit 84 | self.press_key(data.KeyboardShortcuts['Enter']) 85 | 86 | def wait_for_screen_change(self, timeout=30, tolerance=2.5, load_time=3): 87 | # wait for screen to change 88 | self.debug('Waiting for screen to change') 89 | if self.monitor_game_screen(timeout=timeout, tolerance=tolerance): 90 | # wait for screen to load 91 | self.debug('Waiting for screen to load (%d sec)' % load_time) 92 | self.sleep(load_time) 93 | 94 | def wait_for_box_appear(self, box_name, box_color=None, timeout=30): 95 | # set box color 96 | if box_color is None: 97 | if box_name in data.Colors: 98 | box_color = data.Colors[box_name] 99 | else: 100 | return False 101 | # wait for box to appear 102 | elapsed_time = 0 103 | while elapsed_time < timeout: 104 | # wait 1 second 105 | self.sleep(1) 106 | # check for pause or suspend 107 | self.pause_event.wait() 108 | if self.suspend: return False 109 | # check box color 110 | location = self.get_box_location(box_name) 111 | screen = tools.screen_game(location) 112 | percentage = tools.get_color_percentage(screen, box_color) 113 | has_appeared = percentage >= 99 114 | debug_level = DebugLevel.Normal if has_appeared else DebugLevel.High 115 | self.debug('{} has appeared: {}, percentage: {}%, timeout: {}'.format(box_name, has_appeared, percentage, timeout), debug_level) 116 | if has_appeared: 117 | return True 118 | elapsed_time += 1 119 | # if box did not appear before timeout 120 | return False 121 | 122 | def get_box_location(self, box_name): 123 | if self.game_location: 124 | game_x, game_y, game_width, game_height = self.game_location 125 | box_window_width, box_window_height = data.Boxes[box_name]['windowSize'] 126 | x, y = tools.adjust_click_position(data.Boxes[box_name]['x'], data.Boxes[box_name]['y'], box_window_width, box_window_height, game_x, game_y, game_width, game_height) 127 | width = data.Boxes[box_name]['width'] 128 | height = data.Boxes[box_name]['height'] 129 | return (x, y, width, height) 130 | else: 131 | return None 132 | 133 | def click(self, coord, double=False): 134 | # adjust coordinates 135 | if self.game_location: 136 | game_x, game_y, game_width, game_height = self.game_location 137 | #print('game_x: %d, game_y: %d, game_width: %d, game_height: %d' % (game_x, game_y, game_width, game_height)) 138 | x, y = tools.adjust_click_position(coord['x'], coord['y'], coord['width'], coord['height'], game_x, game_y, game_width, game_height) 139 | else: 140 | x, y = (coord['x'], coord['y']) 141 | # click 142 | self.debug('Click on x: %d, y: %d, double: %s' % (x, y, double), DebugLevel.High) 143 | tools.perform_click(x, y, double) 144 | 145 | def double_click(self, coord): 146 | self.click(coord, True) 147 | 148 | def press_key(self, key): 149 | # press key 150 | self.debug('Press key: ' + key, DebugLevel.High) 151 | tools.press_key(key) 152 | 153 | def type_text(self, text): 154 | # type text 155 | self.debug('Type text: ' + text, DebugLevel.High) 156 | tools.type_text(text) 157 | 158 | def scroll(self, value): 159 | self.debug('Scroll to: %d' % value, DebugLevel.High) 160 | # save mouse position 161 | mouse_position = tools.get_mouse_position() 162 | if self.game_location: 163 | # get game center 164 | x, y = tools.coordinates_center(self.game_location) 165 | self.sleep(1) 166 | else: 167 | x, y = (None, None) 168 | # scroll 169 | tools.scroll_to(value, x, y) 170 | # wait for scroll to end 171 | self.sleep(1) 172 | # get back mouse to initial position 173 | tools.move_mouse_to(mouse_position) 174 | 175 | def monitor_game_screen(self, timeout=10, tolerance=0.0, screen=None, location=None, wait_after_timeout=True): 176 | if self.game_location or location: 177 | # screen game 178 | if location is None: 179 | location = self.game_location 180 | if screen is None: 181 | prev_screen = tools.screen_game(location) 182 | else: 183 | prev_screen = screen 184 | elapsed_time = 0 185 | # wait for game screen to change 186 | while elapsed_time < timeout: 187 | # wait 1 second 188 | self.sleep(1) 189 | # check for pause or suspend 190 | self.pause_event.wait() 191 | if self.suspend: return False 192 | # take a new screen & compare it with the previous one 193 | new_screen = tools.screen_game(location) 194 | if new_screen.size != prev_screen.size: 195 | self.debug('Screen size has changed, retry', DebugLevel.High) 196 | else: 197 | diff_percent = round(imgcompare.image_diff_percent(prev_screen, new_screen), 2) 198 | has_changed = diff_percent > tolerance 199 | debug_level = DebugLevel.Normal if has_changed else DebugLevel.High 200 | self.debug('Game screen has changed: {}, diff: {}%, tolerance: {}, timeout: {}'.format(has_changed, diff_percent, tolerance, timeout), debug_level) 201 | if has_changed: 202 | return True 203 | prev_screen = new_screen 204 | elapsed_time += 1 205 | # if game screen hasn't change before timeout 206 | if wait_after_timeout and elapsed_time == timeout: 207 | self.pause() 208 | self.log('Game screen did not change', LogType.Error) 209 | 210 | return False 211 | 212 | def monitor_internet_state(self, timeout=30): 213 | elapsed_time = 0 214 | reported = False 215 | while elapsed_time < timeout: 216 | # check for pause or suspend 217 | self.pause_event.wait() 218 | if self.suspend: return 219 | # get internet state 220 | state = tools.internet_on() 221 | if state: 222 | if reported: 223 | GObject.idle_add(self.parent.set_internet_state, state) 224 | return 225 | else: 226 | # print state 227 | self.debug(tools.print_internet_state(state), DebugLevel.High) 228 | if not reported: 229 | GObject.idle_add(self.parent.set_internet_state, state) 230 | reported = True 231 | # wait 1 second, before recheck 232 | time.sleep(1) 233 | elapsed_time += 1 234 | # if timeout reached 235 | if elapsed_time == timeout: 236 | self.pause() 237 | self.log('Unable to connect to the internet', LogType.Error) 238 | 239 | def sleep(self, duration=1): 240 | elapsed_time = 0 241 | while elapsed_time < duration: 242 | # check for pause or suspend 243 | self.pause_event.wait() 244 | if self.suspend: return 245 | # sleep 246 | time.sleep(1) 247 | # check internet state before continue 248 | self.monitor_internet_state() 249 | elapsed_time += 1 250 | -------------------------------------------------------------------------------- /gui/dev.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | import gi 5 | gi.require_version('Gtk', '3.0') 6 | from gi.repository import Gtk, Gdk, GdkPixbuf 7 | from lib import tools, data, convert 8 | from .custom import CustomTreeView, CustomComboBox, MenuButton, SpinButton, ButtonBox 9 | from .dialog import CopyTextDialog 10 | from threading import Thread 11 | 12 | class DevToolsWidget(Gtk.Box): 13 | 14 | def __init__(self, parent): 15 | Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=5) 16 | self.set_border_width(10) 17 | self.parent = parent 18 | #self.parent.connect('button-press-event', self.on_click) 19 | ## Pixel 20 | top_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) 21 | top_box.add(Gtk.Label('Pixel', xalign=0, use_markup=True)) 22 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) 23 | top_box.pack_start(hbox, True, True, 0) 24 | self.add(top_box) 25 | # TreeView 26 | model = Gtk.ListStore(GdkPixbuf.Pixbuf, int, int, int, int, str) 27 | text_renderer = Gtk.CellRendererText() 28 | columns = [ 29 | Gtk.TreeViewColumn('', Gtk.CellRendererPixbuf(), pixbuf=0), 30 | Gtk.TreeViewColumn('X', text_renderer, text=1), 31 | Gtk.TreeViewColumn('Y', text_renderer, text=2), 32 | Gtk.TreeViewColumn('Width', text_renderer, text=3), 33 | Gtk.TreeViewColumn('Height', text_renderer, text=4), 34 | Gtk.TreeViewColumn('Color', text_renderer, text=5) 35 | ] 36 | self.tree_view = CustomTreeView(model, columns) 37 | self.tree_view.connect('button-press-event', self.on_tree_view_double_clicked) 38 | self.tree_view.connect('selection-changed', self.on_tree_view_selection_changed) 39 | hbox.pack_start(self.tree_view, True, True, 0) 40 | # Select 41 | buttons_box = ButtonBox(orientation=Gtk.Orientation.VERTICAL, centered=True, linked=True) 42 | hbox.add(buttons_box) 43 | self.select_pixel_button = Gtk.Button() 44 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(tools.get_full_path('icons/crosshair.png'), 16, 16) 45 | #pixbuf = Gdk.Cursor(Gdk.CursorType.CROSSHAIR).get_image().scale_simple(18, 18, GdkPixbuf.InterpType.BILINEAR) 46 | self.select_pixel_button.set_image(Gtk.Image(pixbuf=pixbuf)) 47 | self.select_pixel_button.set_tooltip_text('Select') 48 | self.select_pixel_button.connect('clicked', self.on_select_pixel_button_clicked) 49 | buttons_box.add(self.select_pixel_button) 50 | # Simulate 51 | self.simulate_click_button = Gtk.Button() 52 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(tools.get_full_path('icons/hand.png'), 16, 16) 53 | #pixbuf = Gdk.Cursor(Gdk.CursorType.HAND1).get_image().scale_simple(18, 18, GdkPixbuf.InterpType.BILINEAR) 54 | self.simulate_click_button.set_image(Gtk.Image(pixbuf=pixbuf)) 55 | self.simulate_click_button.set_tooltip_text('Simulate Click') 56 | self.simulate_click_button.set_sensitive(False) 57 | self.simulate_click_button.connect('clicked', self.on_simulate_click_button_clicked) 58 | buttons_box.add(self.simulate_click_button) 59 | # Delete 60 | self.delete_pixel_button = Gtk.Button() 61 | self.delete_pixel_button.set_image(Gtk.Image(icon_name='edit-delete-symbolic')) 62 | self.delete_pixel_button.set_tooltip_text('Delete') 63 | self.delete_pixel_button.set_sensitive(False) 64 | self.delete_pixel_button.connect('clicked', self.on_delete_pixel_button_clicked) 65 | buttons_box.add(self.delete_pixel_button) 66 | ## Key Press 67 | bottom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 68 | self.add(bottom_box) 69 | vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) 70 | bottom_box.pack_start(vbox, True, True, 0) 71 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) 72 | vbox.add(hbox) 73 | hbox.add(Gtk.Label('Key Press', xalign=0, use_markup=True)) 74 | # Label 75 | self.keys_label = Gtk.Label() 76 | hbox.add(self.keys_label) 77 | # ComboBox 78 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) 79 | vbox.add(hbox) 80 | self.keys_combo = CustomComboBox(data.KeyboardShortcuts, sort=True) 81 | self.keys_combo.connect('changed', self.on_keys_combo_changed) 82 | hbox.pack_start(self.keys_combo, True, True, 0) 83 | # Simulate 84 | self.simulate_key_press_button = Gtk.Button() 85 | self.simulate_key_press_button.set_image(Gtk.Image(icon_name='input-keyboard')) 86 | self.simulate_key_press_button.set_tooltip_text('Simulate') 87 | self.simulate_key_press_button.set_sensitive(False) 88 | self.simulate_key_press_button.connect('clicked', self.on_simulate_key_press_button_clicked) 89 | hbox.add(self.simulate_key_press_button) 90 | ## Scroll 91 | vbox.add(Gtk.Label('Scroll', xalign=0, use_markup=True)) 92 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) 93 | vbox.add(hbox) 94 | # Direction 95 | self.scroll_direction_combo = CustomComboBox(['up', 'down']) 96 | self.scroll_direction_combo.set_active(1) 97 | hbox.pack_start(self.scroll_direction_combo, True, True, 0) 98 | # Value 99 | self.scroll_menu_button = MenuButton(text='1', position=Gtk.PositionType.TOP) 100 | self.scroll_spin_button = SpinButton(min=1, max=10) 101 | self.scroll_spin_button.connect('value-changed', lambda button: self.scroll_menu_button.set_label(str(button.get_value_as_int()))) 102 | self.scroll_menu_button.add(self.scroll_spin_button) 103 | hbox.add(self.scroll_menu_button) 104 | # Simulate 105 | simulate_scroll_button = Gtk.Button() 106 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(tools.get_full_path('icons/scroll.png'), 16, 16) 107 | #pixbuf = Gdk.Cursor(Gdk.CursorType.SB_V_DOUBLE_ARROW).get_image().scale_simple(18, 18, GdkPixbuf.InterpType.BILINEAR) 108 | simulate_scroll_button.set_image(Gtk.Image(pixbuf=pixbuf)) 109 | simulate_scroll_button.set_tooltip_text('Simulate') 110 | simulate_scroll_button.connect('clicked', self.on_simulate_scroll_button_clicked) 111 | hbox.add(simulate_scroll_button) 112 | 113 | def on_simulate_scroll_button_clicked(self, button): 114 | # get scroll value 115 | direction = self.scroll_direction_combo.get_active_text() 116 | value = self.scroll_spin_button.get_value_as_int() 117 | clicks = value if direction == 'up' else -value 118 | if self.parent.game_window and not self.parent.game_window.is_destroyed() and self.parent.game_window_location: 119 | # get the center of the game location 120 | x, y = tools.coordinates_center(self.parent.game_window_location) 121 | else: 122 | x, y = (None, None) 123 | # scroll 124 | self.parent.focus_game() 125 | self.parent.debug('Scroll: %d' % clicks) 126 | tools.scroll_to(clicks, x, y, 0.5) 127 | 128 | def on_keys_combo_changed(self, combo): 129 | selected = combo.get_active_text() 130 | self.keys_label.set_text('(' + data.KeyboardShortcuts[selected] + ')') 131 | if not self.simulate_key_press_button.get_sensitive(): 132 | self.simulate_key_press_button.set_sensitive(True) 133 | 134 | def add_pixel(self, location): 135 | x, y, width, height, color = location 136 | # create pixbuf 137 | pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, 10, 10) 138 | pixel = convert.rgb2pixel(color) 139 | pixbuf.fill(pixel) 140 | # append to treeview 141 | self.tree_view.append_row([pixbuf, x, y, width, height, str(color)]) 142 | self.select_pixel_button.set_sensitive(True) 143 | self.parent.set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW)) 144 | 145 | def on_select_pixel_button_clicked(self, button): 146 | button.set_sensitive(False) 147 | self.parent.set_cursor(Gdk.Cursor(Gdk.CursorType.CROSSHAIR)) 148 | # wait for click 149 | Thread(target=self.parent.wait_for_click, args=(self.add_pixel, self.parent.game_window_location)).start() 150 | 151 | def on_simulate_click_button_clicked(self, button): 152 | # get click coordinates 153 | selected_row = self.tree_view.get_selected_row() 154 | x, y, width, height = (selected_row[1], selected_row[2], selected_row[3], selected_row[4]) 155 | #print('x: %d, y: %d, width: %d, height: %d' % (x, y, width, height)) 156 | # adjust for game area 157 | if self.parent.game_window and not self.parent.game_window.is_destroyed() and self.parent.game_window_location: 158 | game_x, game_y, game_width, game_height = self.parent.game_window_location 159 | #print('game_x: %d, game_y: %d, game_width: %d, game_height: %d' % (game_x, game_y, game_width, game_height)) 160 | click_x, click_y = tools.adjust_click_position(x, y, width, height, game_x, game_y, game_width, game_height) 161 | else: 162 | click_x = x 163 | click_y = y 164 | # perform click 165 | self.parent.debug('Simulate click on x: %d, y: %d' % (click_x, click_y)) 166 | tools.perform_click(click_x, click_y) 167 | 168 | def on_delete_pixel_button_clicked(self, button): 169 | self.tree_view.remove_selected_row() 170 | 171 | def on_simulate_key_press_button_clicked(self, button): 172 | selected = self.keys_combo.get_active_text() 173 | key = data.KeyboardShortcuts[selected] 174 | self.parent.focus_game() 175 | self.parent.debug('Press key: %s' % key) 176 | tools.press_key(key, 0.5) 177 | 178 | def on_click(self, widget, event): 179 | print('x: %d, y: %d' % (event.x, event.y)) 180 | 181 | def on_tree_view_double_clicked(self, widget, event): 182 | if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS: 183 | selected_row = self.tree_view.get_selected_row() 184 | if selected_row: 185 | x, y, width, height, color = (selected_row[1], selected_row[2], selected_row[3], selected_row[4], selected_row[5]) 186 | CopyTextDialog(self.parent, "{'x': %d, 'y': %d, 'width': %d, 'height': %d, 'color': %s}" % (x, y, width, height, color)) 187 | 188 | def on_tree_view_selection_changed(self, selection): 189 | model, tree_iter = selection.get_selected() 190 | if tree_iter is None: 191 | self.simulate_click_button.set_sensitive(False) 192 | self.delete_pixel_button.set_sensitive(False) 193 | else: 194 | if not self.simulate_click_button.get_sensitive(): 195 | self.simulate_click_button.set_sensitive(True) 196 | if not self.delete_pixel_button.get_sensitive(): 197 | self.delete_pixel_button.set_sensitive(True) 198 | -------------------------------------------------------------------------------- /lib/tools.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | import sys 5 | import os 6 | import gi 7 | gi.require_version('Gtk', '3.0') 8 | gi.require_version('Wnck', '3.0') 9 | from gi.repository import Gtk, Gdk, GdkX11, Wnck 10 | from datetime import datetime 11 | from Xlib import display, X, Xatom 12 | from PIL import Image 13 | from . import parser 14 | import pyautogui 15 | import time 16 | import socket 17 | import webbrowser 18 | 19 | disp = display.Display() 20 | root = disp.screen().root 21 | NET_FRAME_EXTENTS = disp.intern_atom('_NET_FRAME_EXTENTS') 22 | 23 | # Return active game window(s) list 24 | def get_game_window_list(): 25 | game_window_list = {} 26 | try: 27 | screen = Wnck.Screen.get_default() 28 | screen.force_update() # recommended per Wnck documentation 29 | window_list = screen.get_windows() 30 | for window in window_list: 31 | window_name = window.get_name() 32 | instance_name = window.get_class_instance_name() 33 | #print('[' + instance_name + '] ' + window_name) 34 | if instance_name == 'dofus.exe' or instance_name == 'dofus': # use 'sun-awt-X11-XFramePeer' for Wakfu 35 | if window_name in game_window_list: 36 | name = '%s (%d)' % (window_name, len(game_window_list)+1) 37 | else: 38 | name = window_name 39 | game_window_list[name] = window.get_xid() 40 | except Exception as ex: 41 | print(ex.message) 42 | return game_window_list 43 | 44 | # Return screen size 45 | def get_screen_size(): 46 | #screen = Gdk.Screen.get_default() 47 | #return (screen.get_width(), screen.get_height()) 48 | return pyautogui.size() 49 | 50 | # Activate a window 51 | def activate_window(window): 52 | screen = Wnck.Screen.get_default() 53 | screen.force_update() 54 | wnckwin = [win for win in screen.get_windows() if win.get_xid() == window.get_xid()][0] 55 | wnckwin.activate(GdkX11.x11_get_server_time(window)) 56 | 57 | # Return game window 58 | def get_game_window(window_xid): 59 | gdk_display = GdkX11.X11Display.get_default() 60 | game_window = GdkX11.X11Window.foreign_new_for_display(gdk_display, window_xid) 61 | return game_window 62 | 63 | # Return game window decoration height 64 | def get_game_window_decoration_height(window_xid): 65 | window = disp.create_resource_object('window', window_xid) 66 | window_decoration_property = window.get_full_property(NET_FRAME_EXTENTS, Xatom.CARDINAL).value # return array(left, right, top, bottom) of borders width 67 | window_decoration_height = int(window_decoration_property[2]) + int(window_decoration_property[3]) 68 | return window_decoration_height 69 | 70 | # Return absolute path 71 | def get_full_path(rel_path): 72 | dir_of_py_file = os.path.dirname(__file__) 73 | root_dir = os.path.join(dir_of_py_file, '..') 74 | relative_path = os.path.join(root_dir, rel_path) 75 | absolute_path = os.path.abspath(relative_path) 76 | return absolute_path 77 | 78 | # Return internet state 79 | def internet_on(host='8.8.8.8', port=53, timeout=3): 80 | ''' 81 | Host: 8.8.8.8 (google-public-dns-a.google.com) 82 | OpenPort: 53/tcp 83 | Service: domain (DNS/TCP) 84 | ''' 85 | try: 86 | socket.setdefaulttimeout(timeout) 87 | socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) 88 | return True 89 | except Exception as ex: 90 | #print(ex.message) 91 | return False 92 | 93 | # Return internet state as a string 94 | def print_internet_state(state=None): 95 | if state is None: 96 | state = internet_on() 97 | return 'Internet is ' + ('on' if state else 'off') 98 | 99 | # Take a screenshot of given window 100 | def take_window_screenshot(window, save_to='screenshot'): 101 | size = window.get_geometry() 102 | pb = Gdk.pixbuf_get_from_window(window, 0, 0, size.width, size.height) 103 | pb.savev(save_to + '.png', 'png', (), ()) 104 | 105 | # Return a screenshot of the game 106 | def screen_game(region, save_to=None): 107 | x, y, width, height = region 108 | try: 109 | raw = root.get_image(x, y, width, height, X.ZPixmap, 0xffffffff) 110 | if hasattr(Image, 'frombytes'): 111 | # for Pillow 112 | screenshot = Image.frombytes('RGB', (width, height), raw.data, 'raw', 'BGRX') 113 | else: 114 | # for PIL 115 | screenshot = Image.fromstring('RGB', (width, height), raw.data, 'raw', 'BGRX') 116 | if save_to is not None: 117 | screenshot.save(save_to + '.png') 118 | except: 119 | filename = save_to + '.png' if save_to is not None else None 120 | screenshot = pyautogui.screenshot(filename, region) 121 | return screenshot 122 | 123 | # Return pixel color of given x, y coordinates 124 | def get_pixel_color(x, y): 125 | pixel = screen_game((x, y, 1, 1)) 126 | rgb = pixel.getpixel((0, 0)) 127 | return rgb 128 | 129 | # Return date as a string in the given format 130 | def get_date(format='%d-%m-%y'): 131 | return datetime.now().strftime(format) 132 | 133 | # Return time as a string 134 | def get_time(): 135 | return get_date('%H:%M:%S') 136 | 137 | # Return date & time as a string 138 | def get_date_time(): 139 | return get_date('%d-%m-%y_%H-%M-%S') 140 | 141 | # Return timestamp 142 | def get_timestamp(as_int=True): 143 | timestamp = time.mktime(datetime.now().timetuple()) 144 | return int(timestamp) if as_int else timestamp 145 | 146 | # Save text to file 147 | def save_text_to_file(text, filename, mode='w'): 148 | file = open(filename, mode) 149 | file.write(text) 150 | file.close() 151 | 152 | # Return file content 153 | def read_file(filename): 154 | if os.path.isfile(filename): 155 | file = open(filename, 'r') 156 | content = file.read() 157 | file.close() 158 | return content 159 | else: 160 | return None 161 | 162 | # Return platform name 163 | def get_platform(): 164 | return sys.platform 165 | 166 | # Checks platform name 167 | def platform_is(platform_name, use_startswith=False): 168 | if use_startswith: 169 | return sys.platform.startswith(platform_name) 170 | else: 171 | return sys.platform == platform_name 172 | 173 | # Return command line arguments 174 | def get_cmd_args(): 175 | return sys.argv[1:] 176 | 177 | # Return widget location 178 | def get_widget_location(widget): 179 | if widget: 180 | # get widget allocation (relative to parent) 181 | allocation = widget.get_allocation() 182 | # get widget position (relative to root window) 183 | if type(widget) in (Gtk.DrawingArea, Gtk.EventBox, Gtk.Socket): 184 | pos = widget.get_window().get_origin() 185 | return (pos.x, pos.y, allocation.width, allocation.height) 186 | else: 187 | pos_x, pos_y = widget.get_window().get_root_coords(allocation.x, allocation.y) 188 | return (pos_x, pos_y, allocation.width, allocation.height) 189 | else: 190 | return None 191 | 192 | # Check if position is inside given bounds 193 | def position_is_inside_bounds(pos_x, pos_y, bounds_x, bounds_y, bounds_width, bounds_height): 194 | if pos_x > bounds_x and pos_x < (bounds_x + bounds_width) and pos_y > bounds_y and pos_y < (bounds_y + bounds_height): 195 | return True 196 | else: 197 | return False 198 | 199 | # Fit position coordinates to given destination 200 | def fit_position_to_destination(x, y, window_width, window_height, dest_width, dest_height): 201 | # new coordinate = old coordinate / (window size / destination size) 202 | new_x = x / (window_width / float(dest_width)) 203 | new_y = y / (window_height / float(dest_height)) 204 | return (int(new_x), int(new_y)) 205 | 206 | # Adjust click position 207 | def adjust_click_position(click_x, click_y, window_width, window_height, dest_x, dest_y, dest_width, dest_height): 208 | # get screen size 209 | screen_width, screen_height = pyautogui.size() 210 | if screen_width > window_width and screen_height > window_height: 211 | # fit position to destination size 212 | new_x, new_y = fit_position_to_destination(click_x, click_y, window_width, window_height, dest_width, dest_height) 213 | #print('new_x: %d, new_y: %d, dest_x: %d, dest_y: %d' % (new_x, new_y, dest_x, dest_y)) 214 | # scale to screen 215 | x = new_x + dest_x 216 | y = new_y + dest_y 217 | else: 218 | x = click_x 219 | y = click_y 220 | return (x, y) 221 | 222 | # Perform a simple click or double click on x, y position 223 | def perform_click(x, y, double=False): 224 | old_position = pyautogui.position() 225 | if double: 226 | pyautogui.doubleClick(x=x, y=y, interval=0.1) 227 | else: 228 | pyautogui.click(x=x, y=y) 229 | pyautogui.moveTo(old_position) 230 | 231 | # Press key 232 | def press_key(key, interval=None): 233 | if interval is not None: 234 | time.sleep(interval) 235 | keys = parser.parse_key(key) 236 | count = len(keys) 237 | if count == 1: 238 | pyautogui.press(keys[0]) 239 | elif count == 2: 240 | pyautogui.hotkey(keys[0], keys[1]) 241 | 242 | # Type text 243 | def type_text(text, interval=0.1): 244 | for c in text: 245 | if c.isdigit(): 246 | pyautogui.hotkey('shift', c, interval=0.1) 247 | else: 248 | pyautogui.press(c) 249 | time.sleep(interval) 250 | 251 | # Scroll to value 252 | def scroll_to(value, x=None, y=None, interval=None): 253 | if interval is not None: 254 | time.sleep(interval) 255 | pyautogui.scroll(value, x, y) 256 | 257 | # Create directory(s) if not exist 258 | def create_directory(directory): 259 | if not os.path.exists(directory): 260 | os.makedirs(directory) 261 | 262 | # Check if color matches expected color 263 | def color_matches(color, expected_color, tolerance=0): 264 | r, g, b = color 265 | red, green, blue = expected_color 266 | return (abs(r - red) <= tolerance) and (abs(g - green) <= tolerance) and (abs(b - blue) <= tolerance) 267 | 268 | # Return the percentage of a color in an image 269 | def get_color_percentage(image, expected_color, tolerance=10): 270 | # get image colors 271 | width, height = image.size 272 | image = image.convert('RGB') 273 | colors = image.getcolors(width * height) 274 | # check if the expected color exist 275 | expected_color_count = 0 276 | for count, color in colors: 277 | if color_matches(color, expected_color, tolerance): 278 | expected_color_count += count 279 | # convert to percentage 280 | if height == 0: height = 1 281 | if width == 0: width = 1 282 | percentage = ((expected_color_count / height) / float(width)) * 100 283 | return round(percentage, 2) 284 | 285 | # Return the dominant color in an image & his percentage 286 | def get_dominant_color(image): 287 | # get image colors 288 | width, height = image.size 289 | image = image.convert('RGB') 290 | colors = image.getcolors(width * height) 291 | # get dominant color 292 | count, color = max(colors, key=lambda color: color[0]) 293 | # convert to percentage 294 | if height == 0: height = 1 295 | if width == 0: width = 1 296 | percentage = ((count / height) / float(width)) * 100 297 | return (color, round(percentage, 2)) 298 | 299 | # Open given file in text editor 300 | def open_file_in_editor(filename): 301 | editor = os.getenv('EDITOR') 302 | if editor: 303 | os.system('%s %s' % (editor, filename)) 304 | else: 305 | webbrowser.open(filename) 306 | 307 | # Wait for mouse event 308 | def wait_for_mouse_event(event): 309 | pyautogui.waitForMouseEvent(event) 310 | 311 | # Return given coordinates center 312 | def coordinates_center(coords): 313 | return (coords[0] + int(coords[2] / 2), coords[1] + int(coords[3] / 2)) 314 | 315 | # Return mouse position 316 | def get_mouse_position(): 317 | return pyautogui.position() 318 | 319 | # Return screen size 320 | def get_screen_size(): 321 | return pyautogui.size() 322 | 323 | # Move mouse to position 324 | def move_mouse_to(position): 325 | pyautogui.moveTo(position) 326 | -------------------------------------------------------------------------------- /threads/farming.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | from lib.shared import DebugLevel 5 | from lib import data, tools, parser 6 | from .travel import TravelThread 7 | 8 | class FarmingThread(TravelThread): 9 | 10 | def __init__(self, parent, game_location): 11 | TravelThread.__init__(self, parent, game_location) 12 | self.save_dragodindes_images = parent.settings['Farming']['SaveDragodindesImages'] 13 | 14 | def get_dragodinde_spec(self, name, dragodinde_image): 15 | # crop dragodinde image 16 | box = data.Boxes[name] 17 | x, y, w, h = (box['x'], box['y'], box['width'], box['height']) 18 | image = dragodinde_image.crop((x, y, w+x, h+y)) 19 | # get specification percentage & state 20 | state = data.DragodindeStats.Full 21 | percentage = tools.get_color_percentage(image, data.Colors['Full']) 22 | if percentage == 0: 23 | state = data.DragodindeStats.InProgress 24 | percentage = tools.get_color_percentage(image, data.Colors['In Progress']) 25 | if percentage == 0: 26 | state = data.DragodindeStats.Empty 27 | 28 | return [percentage, state] 29 | 30 | def get_dragodinde_stats(self, dragodinde_image): 31 | if dragodinde_image: 32 | # get dragodinde specifications (percent & state) 33 | energy = self.get_dragodinde_spec('Dragodinde Energy', dragodinde_image) 34 | amour = self.get_dragodinde_spec('Dragodinde Amour', dragodinde_image) 35 | maturity = self.get_dragodinde_spec('Dragodinde Maturity', dragodinde_image) 36 | endurance = self.get_dragodinde_spec('Dragodinde Endurance', dragodinde_image) 37 | serenity = self.get_dragodinde_spec('Dragodinde Serenity', dragodinde_image) 38 | # correct energy state 39 | if energy[1] == data.DragodindeStats.Full and energy[0] < data.DragodindeEnergy.Max: 40 | energy[1] = data.DragodindeStats.InProgress 41 | # set serenity state 42 | if serenity[0] >= data.DragodindeSerenity.MaxMedium: 43 | serenity[1] = data.DragodindeSerenity.Positive 44 | elif serenity[1] == data.DragodindeStats.Full: 45 | serenity[1] = data.DragodindeSerenity.Medium 46 | else: 47 | serenity[1] = data.DragodindeSerenity.Negative 48 | # return dragodinde stats 49 | stats = (energy, amour, maturity, endurance, serenity) 50 | self.debug('Energy: {0[0][0]}% ({0[0][1]}), Amour: {0[1][0]}% ({0[1][1]}), Maturity: {0[2][0]}% ({0[2][1]}), Endurance: {0[3][0]}% ({0[3][1]}), Serenity: {0[4][0]}% ({0[4][1]})'.format(stats)) 51 | return stats 52 | else: 53 | return None 54 | 55 | def take_dragodinde_image(self, name, location=None): 56 | # get location 57 | if location is None: 58 | location = self.get_box_location('Dragodinde Card') 59 | # take dragodinde image 60 | if location: 61 | if self.save_dragodindes_images: 62 | # create directory(s) to store dragodindes images 63 | directory = tools.get_full_path('dragodindes') + '/' + tools.get_date() 64 | tools.create_directory(directory) 65 | save_to = directory + '/' + name 66 | else: 67 | save_to = None 68 | return tools.screen_game(location, save_to) 69 | else: 70 | return None 71 | 72 | def move_dragodinde(self, action, dragodinde_image=None, dragodinde_location=None): 73 | if dragodinde_location is None: 74 | dragodinde_location = self.get_box_location('Dragodinde Card') 75 | if dragodinde_image is None: 76 | dragodinde_image = tools.screen_game(dragodinde_location) 77 | self.press_key(data.KeyboardShortcuts[action]) 78 | self.monitor_game_screen(screen=dragodinde_image, location=dragodinde_location) 79 | 80 | def move_dragodinde_to_inventory(self, dragodinde_image=None, dragodinde_location=None): 81 | self.debug('Moving dragodinde to inventory') 82 | self.move_dragodinde('Exchange', dragodinde_image) 83 | return True 84 | 85 | def move_dragodinde_to_enclos(self, dragodinde_image=None, dragodinde_location=None): 86 | self.debug('Moving dragodinde to enclos') 87 | self.move_dragodinde('Elevate', dragodinde_image) 88 | return True 89 | 90 | def move_dragodinde_to_cowshed(self, dragodinde_image=None, dragodinde_location=None): 91 | self.debug('Moving dragodinde to cowshed') 92 | self.move_dragodinde('Store', dragodinde_image) 93 | return True 94 | 95 | def enclos_is_empty(self): 96 | location = self.get_box_location('Enclos First Place') 97 | screen = tools.screen_game(location) 98 | empty_percentage = tools.get_color_percentage(screen, data.Colors['Empty Enclos']) 99 | selected_percentage = tools.get_color_percentage(screen, data.Colors['Selected Row']) 100 | is_empty = empty_percentage >= 99 or selected_percentage >= 99 101 | debug_level = DebugLevel.Normal if is_empty else DebugLevel.High 102 | self.debug('Enclos is empty: {}, empty percentage: {}%, selected percentage: {}%'.format(is_empty, empty_percentage, selected_percentage), debug_level) 103 | return is_empty 104 | 105 | def get_dragodinde_name(self): 106 | return 'dd_%d' % tools.get_timestamp() 107 | 108 | def manage_enclos(self, enclos_type): 109 | # select dragodinde from enclos 110 | self.debug('Select dragodinde from enclos') 111 | screen = tools.screen_game(self.game_location) 112 | self.click(data.Locations['Select From Enclos']) 113 | # wait for dragodinde card to show 114 | self.debug('Waiting for dragodinde card to show') 115 | self.monitor_game_screen(tolerance=2.5, screen=screen) 116 | # manage dragodinde(s) 117 | dragodinde_number = 0 118 | moved_dragodinde_number = 0 119 | dragodinde_location = self.get_box_location('Dragodinde Card') 120 | while dragodinde_number < 10: 121 | # check for pause or suspend 122 | self.pause_event.wait() 123 | if self.suspend: return 0 124 | # break if enclos is empty 125 | if self.enclos_is_empty(): 126 | break 127 | # take dragodinde image 128 | dragodinde_name = self.get_dragodinde_name() 129 | dragodinde_image = self.take_dragodinde_image(dragodinde_name, dragodinde_location) 130 | # increase dragodindes number 131 | dragodinde_number += 1 132 | # get dragodinde stats 133 | if self.save_dragodindes_images: 134 | self.debug("Get dragodinde '%s' stats" % dragodinde_name) 135 | else: 136 | self.debug('Get dragodinde stats') 137 | stats = self.get_dragodinde_stats(dragodinde_image) 138 | if stats: 139 | Stats = data.DragodindeStats 140 | Serenity = data.DragodindeSerenity 141 | # move dragodinde 142 | dragodinde_moved = False 143 | energy, amour, maturity, endurance, serenity = stats 144 | energy_percent, energy_state = energy 145 | amour_percent, amour_state = amour 146 | maturity_percent, maturity_state = maturity 147 | endurance_percent, endurance_state = endurance 148 | serenity_percent, serenity_state = serenity 149 | # get dragodinde needs 150 | need_energy = energy_state in (Stats.Empty, Stats.InProgress) 151 | need_amour = amour_state in (Stats.Empty, Stats.InProgress) 152 | need_maturity = maturity_state in (Stats.Empty, Stats.InProgress) 153 | need_endurance = endurance_state in (Stats.Empty, Stats.InProgress) 154 | self.debug('Need Energy: %s, Amour: %s, Maturity: %s, Endurance: %s' % (need_energy, need_amour, need_maturity, need_endurance), DebugLevel.High) 155 | # enclos 'Amour' 156 | if enclos_type == 'Amour': 157 | if amour_state == Stats.Full: 158 | dragodinde_moved = self.move_dragodinde_to_inventory(dragodinde_image, dragodinde_location) 159 | # enclos 'Endurance' 160 | elif enclos_type == 'Endurance': 161 | if endurance_state == Stats.Full: 162 | dragodinde_moved = self.move_dragodinde_to_inventory(dragodinde_image, dragodinde_location) 163 | # enclos 'NegativeSerenity' 164 | elif enclos_type == 'NegativeSerenity': 165 | if serenity_state == Serenity.Negative or (serenity_state == Serenity.Medium and need_maturity): 166 | dragodinde_moved = self.move_dragodinde_to_inventory(dragodinde_image, dragodinde_location) 167 | # enclos 'PositiveSerenity' 168 | elif enclos_type == 'PositiveSerenity': 169 | if serenity_state == Serenity.Positive or (serenity_state == Serenity.Medium and need_maturity): 170 | dragodinde_moved = self.move_dragodinde_to_inventory(dragodinde_image, dragodinde_location) 171 | # enclos 'Energy' 172 | elif enclos_type == 'Energy': 173 | if all(state == Stats.Full for state in (energy_state, amour_state, maturity_state, endurance_state)): 174 | dragodinde_moved = self.move_dragodinde_to_cowshed(dragodinde_image, dragodinde_location) 175 | elif energy_state == Stats.Full: 176 | dragodinde_moved = self.move_dragodinde_to_inventory(dragodinde_image, dragodinde_location) 177 | # enclos 'Maturity' 178 | elif enclos_type == 'Maturity': 179 | if maturity_state == Stats.Full or not serenity_state == Serenity.Medium: 180 | dragodinde_moved = self.move_dragodinde_to_inventory(dragodinde_image, dragodinde_location) 181 | # Nothing to do 182 | if not dragodinde_moved: 183 | self.debug('Nothing to do') 184 | else: 185 | moved_dragodinde_number += 1 186 | continue 187 | # break if managed dragodinde number equal 10 188 | if dragodinde_number == 10: 189 | break 190 | else: 191 | continue 192 | # check for pause or suspend 193 | self.pause_event.wait() 194 | if self.suspend: return 0 195 | # check next dragodinde 196 | self.debug('Check next dragodinde') 197 | self.press_key(data.KeyboardShortcuts['Down']) 198 | # break if there is no more dragodinde 199 | if not self.monitor_game_screen(tolerance=0.01, screen=dragodinde_image, location=dragodinde_location, wait_after_timeout=False): 200 | if not self.suspend: 201 | self.debug('No more dragodinde') 202 | break 203 | else: 204 | return 205 | 206 | # print managed dragodindes number when break from loop 207 | free_places = 10 - (dragodinde_number - moved_dragodinde_number) 208 | self.debug('(Enclos) Managed dragodindes: %d, Moved dragodindes: %d, Free places: %d' % (dragodinde_number, moved_dragodinde_number, free_places), DebugLevel.Low) 209 | 210 | # return enclos free places number 211 | return free_places 212 | 213 | def inventory_is_empty(self): 214 | location = self.get_box_location('Inventory First Place') 215 | screen = tools.screen_game(location) 216 | percentage = tools.get_color_percentage(screen, data.Colors['Empty Inventory']) 217 | is_empty = percentage >= 99 218 | debug_level = DebugLevel.Normal if is_empty else DebugLevel.High 219 | self.debug('Inventory is empty: {}, percentage: {}%'.format(is_empty, percentage), debug_level) 220 | return is_empty 221 | 222 | def manage_inventory(self, enclos_type, enclos_free_places): 223 | # select dragodinde from inventory 224 | self.debug('Select dragodinde from inventory') 225 | screen = tools.screen_game(self.game_location) 226 | self.click(data.Locations['Select From Inventory']) 227 | # wait for dragodinde card to show 228 | self.debug('Waiting for dragodinde card to show') 229 | self.monitor_game_screen(tolerance=0.1, screen=screen) 230 | # manage dragodinde(s) 231 | dragodinde_number = 0 232 | moved_dragodinde_number = 0 233 | dragodinde_location = self.get_box_location('Dragodinde Card') 234 | while moved_dragodinde_number < enclos_free_places: 235 | # check for pause or suspend 236 | self.pause_event.wait() 237 | if self.suspend: return 238 | # break if inventory empty 239 | if self.inventory_is_empty(): 240 | break 241 | # take dragodinde image 242 | dragodinde_name = self.get_dragodinde_name() 243 | dragodinde_image = self.take_dragodinde_image(dragodinde_name, dragodinde_location) 244 | # increase dragodindes number 245 | dragodinde_number += 1 246 | # get dragodinde stats 247 | if self.save_dragodindes_images: 248 | self.debug("Get dragodinde '%s' stats" % dragodinde_name) 249 | else: 250 | self.debug('Get dragodinde stats') 251 | stats = self.get_dragodinde_stats(dragodinde_image) 252 | if stats: 253 | Stats = data.DragodindeStats 254 | Serenity = data.DragodindeSerenity 255 | # move dragodinde 256 | dragodinde_moved = False 257 | energy, amour, maturity, endurance, serenity = stats 258 | energy_percent, energy_state = energy 259 | amour_percent, amour_state = amour 260 | maturity_percent, maturity_state = maturity 261 | endurance_percent, endurance_state = endurance 262 | serenity_percent, serenity_state = serenity 263 | # get dragodinde needs 264 | need_energy = energy_state in (Stats.Empty, Stats.InProgress) 265 | need_amour = amour_state in (Stats.Empty, Stats.InProgress) 266 | need_maturity = maturity_state in (Stats.Empty, Stats.InProgress) 267 | need_endurance = endurance_state in (Stats.Empty, Stats.InProgress) 268 | self.debug('Need Energy: %s, Amour: %s, Maturity: %s, Endurance: %s' % (need_energy, need_amour, need_maturity, need_endurance), DebugLevel.High) 269 | # enclos 'Amour' 270 | if enclos_type == 'Amour': 271 | if need_amour and serenity_state == Serenity.Positive: 272 | dragodinde_moved = self.move_dragodinde_to_enclos(dragodinde_image, dragodinde_location) 273 | # enclos 'Endurance' 274 | elif enclos_type == 'Endurance': 275 | if need_endurance and serenity_state == Serenity.Negative: 276 | dragodinde_moved = self.move_dragodinde_to_enclos(dragodinde_image, dragodinde_location) 277 | # enclos 'NegativeSerenity' 278 | elif enclos_type == 'NegativeSerenity': 279 | if ((need_maturity or (need_endurance and not need_amour)) and serenity_state == Serenity.Positive) or (need_endurance and serenity_state == Serenity.Medium and maturity_state == Stats.Full): 280 | dragodinde_moved = self.move_dragodinde_to_enclos(dragodinde_image, dragodinde_location) 281 | # enclos 'PositiveSerenity' 282 | elif enclos_type == 'PositiveSerenity': 283 | if ((need_maturity or (need_amour and not need_endurance)) and serenity_state == Serenity.Negative) or (need_amour and serenity_state == Serenity.Medium and maturity_state == Stats.Full): 284 | dragodinde_moved = self.move_dragodinde_to_enclos(dragodinde_image, dragodinde_location) 285 | # enclos 'Energy' 286 | elif enclos_type == 'Energy': 287 | if need_energy and all(state == Stats.Full for state in (amour_state, maturity_state, endurance_state)): 288 | dragodinde_moved = self.move_dragodinde_to_enclos(dragodinde_image, dragodinde_location) 289 | # enclos 'Maturity' 290 | elif enclos_type == 'Maturity': 291 | if need_maturity and serenity_state == Serenity.Medium: 292 | dragodinde_moved = self.move_dragodinde_to_enclos(dragodinde_image, dragodinde_location) 293 | # Nothing to do 294 | if not dragodinde_moved: 295 | self.debug('Nothing to do') 296 | else: 297 | moved_dragodinde_number += 1 298 | continue 299 | else: 300 | continue 301 | # check for pause or suspend 302 | self.pause_event.wait() 303 | if self.suspend: return 304 | # check next dragodinde 305 | self.debug('Check next dragodinde') 306 | self.press_key(data.KeyboardShortcuts['Down']) 307 | # break if there is no more dragodinde 308 | if not self.monitor_game_screen(tolerance=0.01, screen=dragodinde_image, location=dragodinde_location, wait_after_timeout=False): 309 | if not self.suspend: 310 | self.debug('No more dragodinde') 311 | break 312 | else: 313 | return 314 | 315 | # print managed dragodindes number when break from loop 316 | self.debug('(Inventory) Managed dragodindes: %d, Moved dragodindes: %d' % (dragodinde_number, moved_dragodinde_number), DebugLevel.Low) 317 | 318 | def check_enclos(self, enclos_location, enclos_type): 319 | # get enclos coordinates 320 | enclos = parser.parse_data(data.Enclos, enclos_location) 321 | if enclos and enclos_type in data.EnclosType: 322 | self.debug('Check enclos %s (%s)' % (enclos_location, enclos_type)) 323 | # click on enclos 324 | self.click(enclos) 325 | # wait for enclos to open 326 | self.debug('Waiting for enclos to open') 327 | if self.monitor_game_screen(timeout=30, tolerance=2.5): 328 | # wait for enclos to load 329 | self.sleep(1) 330 | # check for pause or suspend 331 | self.pause_event.wait() 332 | if self.suspend: return 333 | # check enclos 334 | enclos_free_places = 0 335 | if not self.enclos_is_empty(): 336 | enclos_free_places = self.manage_enclos(enclos_type) 337 | else: 338 | enclos_free_places = 10 339 | # check for pause or suspend 340 | self.pause_event.wait() 341 | if self.suspend: return 342 | # check inventory 343 | if enclos_free_places > 0 and not self.inventory_is_empty(): 344 | self.manage_inventory(enclos_type, enclos_free_places) 345 | # check for pause or suspend 346 | self.pause_event.wait() 347 | if self.suspend: return 348 | # close enclos 349 | self.debug('Closing enclos') 350 | screen = tools.screen_game(self.game_location) 351 | self.press_key(data.KeyboardShortcuts['Esc']) 352 | # wait for enclos to close 353 | self.debug('Waiting for enclos to close') 354 | self.monitor_game_screen(tolerance=2.5, screen=screen) 355 | -------------------------------------------------------------------------------- /pyautogui/_pyautogui_osx.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | 4 | try: 5 | import Quartz 6 | except: 7 | assert False, "You must first install pyobjc-core and pyobjc: https://pyautogui.readthedocs.io/en/latest/install.html" 8 | import AppKit 9 | 10 | import pyautogui 11 | from pyautogui import LEFT, MIDDLE, RIGHT 12 | 13 | if sys.platform != 'darwin': 14 | raise Exception('The pyautogui_osx module should only be loaded on an OS X system.') 15 | 16 | 17 | 18 | """ Taken from events.h 19 | /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h 20 | 21 | The *KB dictionaries in pyautogui map a string that can be passed to keyDown(), 22 | keyUp(), or press() into the code used for the OS-specific keyboard function. 23 | 24 | They should always be lowercase, and the same keys should be used across all OSes.""" 25 | keyboardMapping = dict([(key, None) for key in pyautogui.KEY_NAMES]) 26 | keyboardMapping.update({ 27 | 'a': 0x00, # kVK_ANSI_A 28 | 's': 0x01, # kVK_ANSI_S 29 | 'd': 0x02, # kVK_ANSI_D 30 | 'f': 0x03, # kVK_ANSI_F 31 | 'h': 0x04, # kVK_ANSI_H 32 | 'g': 0x05, # kVK_ANSI_G 33 | 'z': 0x06, # kVK_ANSI_Z 34 | 'x': 0x07, # kVK_ANSI_X 35 | 'c': 0x08, # kVK_ANSI_C 36 | 'v': 0x09, # kVK_ANSI_V 37 | 'b': 0x0b, # kVK_ANSI_B 38 | 'q': 0x0c, # kVK_ANSI_Q 39 | 'w': 0x0d, # kVK_ANSI_W 40 | 'e': 0x0e, # kVK_ANSI_E 41 | 'r': 0x0f, # kVK_ANSI_R 42 | 'y': 0x10, # kVK_ANSI_Y 43 | 't': 0x11, # kVK_ANSI_T 44 | '1': 0x12, # kVK_ANSI_1 45 | '!': 0x12, # kVK_ANSI_1 46 | '2': 0x13, # kVK_ANSI_2 47 | '@': 0x13, # kVK_ANSI_2 48 | '3': 0x14, # kVK_ANSI_3 49 | '#': 0x14, # kVK_ANSI_3 50 | '4': 0x15, # kVK_ANSI_4 51 | '$': 0x15, # kVK_ANSI_4 52 | '6': 0x16, # kVK_ANSI_6 53 | '^': 0x16, # kVK_ANSI_6 54 | '5': 0x17, # kVK_ANSI_5 55 | '%': 0x17, # kVK_ANSI_5 56 | '=': 0x18, # kVK_ANSI_Equal 57 | '+': 0x18, # kVK_ANSI_Equal 58 | '9': 0x19, # kVK_ANSI_9 59 | '(': 0x19, # kVK_ANSI_9 60 | '7': 0x1a, # kVK_ANSI_7 61 | '&': 0x1a, # kVK_ANSI_7 62 | '-': 0x1b, # kVK_ANSI_Minus 63 | '_': 0x1b, # kVK_ANSI_Minus 64 | '8': 0x1c, # kVK_ANSI_8 65 | '*': 0x1c, # kVK_ANSI_8 66 | '0': 0x1d, # kVK_ANSI_0 67 | ')': 0x1d, # kVK_ANSI_0 68 | ']': 0x1e, # kVK_ANSI_RightBracket 69 | '}': 0x1e, # kVK_ANSI_RightBracket 70 | 'o': 0x1f, # kVK_ANSI_O 71 | 'u': 0x20, # kVK_ANSI_U 72 | '[': 0x21, # kVK_ANSI_LeftBracket 73 | '{': 0x21, # kVK_ANSI_LeftBracket 74 | 'i': 0x22, # kVK_ANSI_I 75 | 'p': 0x23, # kVK_ANSI_P 76 | 'l': 0x25, # kVK_ANSI_L 77 | 'j': 0x26, # kVK_ANSI_J 78 | "'": 0x27, # kVK_ANSI_Quote 79 | '"': 0x27, # kVK_ANSI_Quote 80 | 'k': 0x28, # kVK_ANSI_K 81 | ';': 0x29, # kVK_ANSI_Semicolon 82 | ':': 0x29, # kVK_ANSI_Semicolon 83 | '\\': 0x2a, # kVK_ANSI_Backslash 84 | '|': 0x2a, # kVK_ANSI_Backslash 85 | ',': 0x2b, # kVK_ANSI_Comma 86 | '<': 0x2b, # kVK_ANSI_Comma 87 | '/': 0x2c, # kVK_ANSI_Slash 88 | '?': 0x2c, # kVK_ANSI_Slash 89 | 'n': 0x2d, # kVK_ANSI_N 90 | 'm': 0x2e, # kVK_ANSI_M 91 | '.': 0x2f, # kVK_ANSI_Period 92 | '>': 0x2f, # kVK_ANSI_Period 93 | '`': 0x32, # kVK_ANSI_Grave 94 | '~': 0x32, # kVK_ANSI_Grave 95 | ' ': 0x31, # kVK_Space 96 | 'space': 0x31, 97 | '\r': 0x24, # kVK_Return 98 | '\n': 0x24, # kVK_Return 99 | 'enter': 0x24, # kVK_Return 100 | 'return': 0x24, # kVK_Return 101 | '\t': 0x30, # kVK_Tab 102 | 'tab': 0x30, # kVK_Tab 103 | 'backspace': 0x33, # kVK_Delete, which is "Backspace" on OS X. 104 | '\b': 0x33, # kVK_Delete, which is "Backspace" on OS X. 105 | 'esc': 0x35, # kVK_Escape 106 | 'escape': 0x35, # kVK_Escape 107 | 'command': 0x37, # kVK_Command 108 | 'shift': 0x38, # kVK_Shift 109 | 'shiftleft': 0x38, # kVK_Shift 110 | 'capslock': 0x39, # kVK_CapsLock 111 | 'option': 0x3a, # kVK_Option 112 | 'optionleft': 0x3a, # kVK_Option 113 | 'alt': 0x3a, # kVK_Option 114 | 'altleft': 0x3a, # kVK_Option 115 | 'ctrl': 0x3b, # kVK_Control 116 | 'ctrlleft': 0x3b, # kVK_Control 117 | 'shiftright': 0x3c, # kVK_RightShift 118 | 'optionright': 0x3d, # kVK_RightOption 119 | 'ctrlright': 0x3e, # kVK_RightControl 120 | 'fn': 0x3f, # kVK_Function 121 | 'f17': 0x40, # kVK_F17 122 | 'volumeup': 0x48, # kVK_VolumeUp 123 | 'volumedown': 0x49, # kVK_VolumeDown 124 | 'volumemute': 0x4a, # kVK_Mute 125 | 'f18': 0x4f, # kVK_F18 126 | 'f19': 0x50, # kVK_F19 127 | 'f20': 0x5a, # kVK_F20 128 | 'f5': 0x60, # kVK_F5 129 | 'f6': 0x61, # kVK_F6 130 | 'f7': 0x62, # kVK_F7 131 | 'f3': 0x63, # kVK_F3 132 | 'f8': 0x64, # kVK_F8 133 | 'f9': 0x65, # kVK_F9 134 | 'f11': 0x67, # kVK_F11 135 | 'f13': 0x69, # kVK_F13 136 | 'f16': 0x6a, # kVK_F16 137 | 'f14': 0x6b, # kVK_F14 138 | 'f10': 0x6d, # kVK_F10 139 | 'f12': 0x6f, # kVK_F12 140 | 'f15': 0x71, # kVK_F15 141 | 'help': 0x72, # kVK_Help 142 | 'home': 0x73, # kVK_Home 143 | 'pageup': 0x74, # kVK_PageUp 144 | 'pgup': 0x74, # kVK_PageUp 145 | 'del': 0x75, # kVK_ForwardDelete 146 | 'delete': 0x75, # kVK_ForwardDelete 147 | 'f4': 0x76, # kVK_F4 148 | 'end': 0x77, # kVK_End 149 | 'f2': 0x78, # kVK_F2 150 | 'pagedown': 0x79, # kVK_PageDown 151 | 'pgdn': 0x79, # kVK_PageDown 152 | 'f1': 0x7a, # kVK_F1 153 | 'left': 0x7b, # kVK_LeftArrow 154 | 'right': 0x7c, # kVK_RightArrow 155 | 'down': 0x7d, # kVK_DownArrow 156 | 'up': 0x7e, # kVK_UpArrow 157 | 'yen': 0x5d, # kVK_JIS_Yen 158 | #'underscore' : 0x5e, # kVK_JIS_Underscore (only applies to Japanese keyboards) 159 | #'comma': 0x5f, # kVK_JIS_KeypadComma (only applies to Japanese keyboards) 160 | 'eisu': 0x66, # kVK_JIS_Eisu 161 | 'kana': 0x68, # kVK_JIS_Kana 162 | }) 163 | 164 | """ 165 | # TODO - additional key codes to add 166 | kVK_ANSI_KeypadDecimal = 0x41, 167 | kVK_ANSI_KeypadMultiply = 0x43, 168 | kVK_ANSI_KeypadPlus = 0x45, 169 | kVK_ANSI_KeypadClear = 0x47, 170 | kVK_ANSI_KeypadDivide = 0x4B, 171 | kVK_ANSI_KeypadEnter = 0x4C, 172 | kVK_ANSI_KeypadMinus = 0x4E, 173 | kVK_ANSI_KeypadEquals = 0x51, 174 | kVK_ANSI_Keypad0 = 0x52, 175 | kVK_ANSI_Keypad1 = 0x53, 176 | kVK_ANSI_Keypad2 = 0x54, 177 | kVK_ANSI_Keypad3 = 0x55, 178 | kVK_ANSI_Keypad4 = 0x56, 179 | kVK_ANSI_Keypad5 = 0x57, 180 | kVK_ANSI_Keypad6 = 0x58, 181 | kVK_ANSI_Keypad7 = 0x59, 182 | kVK_ANSI_Keypad8 = 0x5B, 183 | kVK_ANSI_Keypad9 = 0x5C, 184 | """ 185 | 186 | # add mappings for uppercase letters 187 | for c in 'abcdefghijklmnopqrstuvwxyz': 188 | keyboardMapping[c.upper()] = keyboardMapping[c] 189 | 190 | # Taken from ev_keymap.h 191 | # http://www.opensource.apple.com/source/IOHIDFamily/IOHIDFamily-86.1/IOHIDSystem/IOKit/hidsystem/ev_keymap.h 192 | special_key_translate_table = { 193 | 'KEYTYPE_SOUND_UP': 0, 194 | 'KEYTYPE_SOUND_DOWN': 1, 195 | 'KEYTYPE_BRIGHTNESS_UP': 2, 196 | 'KEYTYPE_BRIGHTNESS_DOWN': 3, 197 | 'KEYTYPE_CAPS_LOCK': 4, 198 | 'KEYTYPE_HELP': 5, 199 | 'POWER_KEY': 6, 200 | 'KEYTYPE_MUTE': 7, 201 | 'UP_ARROW_KEY': 8, 202 | 'DOWN_ARROW_KEY': 9, 203 | 'KEYTYPE_NUM_LOCK': 10, 204 | 'KEYTYPE_CONTRAST_UP': 11, 205 | 'KEYTYPE_CONTRAST_DOWN': 12, 206 | 'KEYTYPE_LAUNCH_PANEL': 13, 207 | 'KEYTYPE_EJECT': 14, 208 | 'KEYTYPE_VIDMIRROR': 15, 209 | 'KEYTYPE_PLAY': 16, 210 | 'KEYTYPE_NEXT': 17, 211 | 'KEYTYPE_PREVIOUS': 18, 212 | 'KEYTYPE_FAST': 19, 213 | 'KEYTYPE_REWIND': 20, 214 | 'KEYTYPE_ILLUMINATION_UP': 21, 215 | 'KEYTYPE_ILLUMINATION_DOWN': 22, 216 | 'KEYTYPE_ILLUMINATION_TOGGLE': 23 217 | } 218 | 219 | def _keyDown(key): 220 | if key not in keyboardMapping or keyboardMapping[key] is None: 221 | return 222 | 223 | if key in special_key_translate_table: 224 | _specialKeyEvent(key, 'down') 225 | else: 226 | _normalKeyEvent(key, 'down') 227 | 228 | def _keyUp(key): 229 | if key not in keyboardMapping or keyboardMapping[key] is None: 230 | return 231 | 232 | if key in special_key_translate_table: 233 | _specialKeyEvent(key, 'up') 234 | else: 235 | _normalKeyEvent(key, 'up') 236 | 237 | 238 | def _normalKeyEvent(key, upDown): 239 | assert upDown in ('up', 'down'), "upDown argument must be 'up' or 'down'" 240 | 241 | try: 242 | if pyautogui.isShiftCharacter(key): 243 | key_code = keyboardMapping[key.lower()] 244 | 245 | event = Quartz.CGEventCreateKeyboardEvent(None, 246 | keyboardMapping['shift'], upDown == 'down') 247 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, event) 248 | # Tiny sleep to let OS X catch up on us pressing shift 249 | time.sleep(0.01) 250 | 251 | else: 252 | key_code = keyboardMapping[key] 253 | 254 | event = Quartz.CGEventCreateKeyboardEvent(None, key_code, upDown == 'down') 255 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, event) 256 | time.sleep(0.01) 257 | 258 | # TODO - wait, is the shift key's keyup not done? 259 | # TODO - get rid of this try-except. 260 | 261 | except KeyError: 262 | raise RuntimeError("Key %s not implemented." % (key)) 263 | 264 | def _specialKeyEvent(key, upDown): 265 | """ Helper method for special keys. 266 | 267 | Source: http://stackoverflow.com/questions/11045814/emulate-media-key-press-on-mac 268 | """ 269 | assert upDown in ('up', 'down'), "upDown argument must be 'up' or 'down'" 270 | 271 | key_code = special_key_translate_table[key] 272 | 273 | ev = AppKit.NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_( 274 | Quartz.NSSystemDefined, # type 275 | (0,0), # location 276 | 0xa00 if upDown == 'down' else 0xb00, # flags 277 | 0, # timestamp 278 | 0, # window 279 | 0, # ctx 280 | 8, # subtype 281 | (key_code << 16) | ((0xa if upDown == 'down' else 0xb) << 8), # data1 282 | -1 # data2 283 | ) 284 | 285 | Quartz.CGEventPost(0, ev.CGEvent()) 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | def _position(): 296 | loc = AppKit.NSEvent.mouseLocation() 297 | return int(loc.x), int(Quartz.CGDisplayPixelsHigh(0) - loc.y) 298 | 299 | 300 | def _size(): 301 | return Quartz.CGDisplayPixelsWide(Quartz.CGMainDisplayID()), Quartz.CGDisplayPixelsHigh(Quartz.CGMainDisplayID()) 302 | 303 | 304 | 305 | def _scroll(clicks, x=None, y=None): 306 | _vscroll(clicks, x, y) 307 | 308 | 309 | """ 310 | According to https://developer.apple.com/library/mac/documentation/Carbon/Reference/QuartzEventServicesRef/Reference/reference.html#//apple_ref/c/func/Quartz.CGEventCreateScrollWheelEvent 311 | "Scrolling movement is generally represented by small signed integer values, typically in a range from -10 to +10. Large values may have unexpected results, depending on the application that processes the event." 312 | The scrolling functions will create multiple events that scroll 10 each, and then scroll the remainder. 313 | """ 314 | 315 | def _vscroll(clicks, x=None, y=None): 316 | _moveTo(x, y) 317 | clicks = int(clicks) 318 | for _ in range(abs(clicks) // 10): 319 | scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent( 320 | None, # no source 321 | Quartz.kCGScrollEventUnitLine, # units 322 | 1, # wheelCount (number of dimensions) 323 | 10 if clicks >= 0 else -10) # vertical movement 324 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent) 325 | 326 | scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent( 327 | None, # no source 328 | Quartz.kCGScrollEventUnitLine, # units 329 | 1, # wheelCount (number of dimensions) 330 | clicks % 10 if clicks >= 0 else -1 * (-clicks % 10)) # vertical movement 331 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent) 332 | 333 | 334 | def _hscroll(clicks, x=None, y=None): 335 | _moveTo(x, y) 336 | clicks = int(clicks) 337 | for _ in range(abs(clicks) // 10): 338 | scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent( 339 | None, # no source 340 | Quartz.kCGScrollEventUnitLine, # units 341 | 2, # wheelCount (number of dimensions) 342 | 0, # vertical movement 343 | 10 if clicks >= 0 else -10) # horizontal movement 344 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent) 345 | 346 | scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent( 347 | None, # no source 348 | Quartz.kCGScrollEventUnitLine, # units 349 | 2, # wheelCount (number of dimensions) 350 | 0, # vertical movement 351 | (clicks % 10) if clicks >= 0 else (-1 * clicks % 10)) # horizontal movement 352 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent) 353 | 354 | 355 | def _mouseDown(x, y, button): 356 | if button == LEFT: 357 | _sendMouseEvent(Quartz.kCGEventLeftMouseDown, x, y, Quartz.kCGMouseButtonLeft) 358 | elif button == MIDDLE: 359 | _sendMouseEvent(Quartz.kCGEventOtherMouseDown, x, y, Quartz.kCGMouseButtonCenter) 360 | elif button == RIGHT: 361 | _sendMouseEvent(Quartz.kCGEventRightMouseDown, x, y, Quartz.kCGMouseButtonRight) 362 | else: 363 | assert False, "button argument not in ('left', 'middle', 'right')" 364 | 365 | 366 | def _mouseUp(x, y, button): 367 | if button == LEFT: 368 | _sendMouseEvent(Quartz.kCGEventLeftMouseUp, x, y, Quartz.kCGMouseButtonLeft) 369 | elif button == MIDDLE: 370 | _sendMouseEvent(Quartz.kCGEventOtherMouseUp, x, y, Quartz.kCGMouseButtonCenter) 371 | elif button == RIGHT: 372 | _sendMouseEvent(Quartz.kCGEventRightMouseUp, x, y, Quartz.kCGMouseButtonRight) 373 | else: 374 | assert False, "button argument not in ('left', 'middle', 'right')" 375 | 376 | 377 | def _click(x, y, button): 378 | if button == LEFT: 379 | _sendMouseEvent(Quartz.kCGEventLeftMouseDown, x, y, Quartz.kCGMouseButtonLeft) 380 | _sendMouseEvent(Quartz.kCGEventLeftMouseUp, x, y, Quartz.kCGMouseButtonLeft) 381 | elif button == MIDDLE: 382 | _sendMouseEvent(Quartz.kCGEventOtherMouseDown, x, y, Quartz.kCGMouseButtonCenter) 383 | _sendMouseEvent(Quartz.kCGEventOtherMouseUp, x, y, Quartz.kCGMouseButtonCenter) 384 | elif button == RIGHT: 385 | _sendMouseEvent(Quartz.kCGEventRightMouseDown, x, y, Quartz.kCGMouseButtonRight) 386 | _sendMouseEvent(Quartz.kCGEventRightMouseUp, x, y, Quartz.kCGMouseButtonRight) 387 | else: 388 | assert False, "button argument not in ('left', 'middle', 'right')" 389 | 390 | def _multiClick(x, y, button, num): 391 | btn = None 392 | down = None 393 | up = None 394 | 395 | if button == LEFT: 396 | btn = Quartz.kCGMouseButtonLeft 397 | down = Quartz.kCGEventLeftMouseDown 398 | up = Quartz.kCGEventLeftMouseUp 399 | elif button == MIDDLE: 400 | btn = Quartz.kCGMouseButtonCenter 401 | down = Quartz.kCGEventOtherMouseDown 402 | up = Quartz.kCGEventOtherMouseUp 403 | elif button == RIGHT: 404 | btn = Quartz.kCGMouseButtonRight 405 | down = Quartz.kCGEventRightMouseDown 406 | up = Quartz.kCGEventRightMouseUp 407 | else: 408 | assert False, "button argument not in ('left', 'middle', 'right')" 409 | return 410 | 411 | mouseEvent = Quartz.CGEventCreateMouseEvent(None, down, (x, y), btn) 412 | Quartz.CGEventSetIntegerValueField(mouseEvent, Quartz.kCGMouseEventClickState, num) 413 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, mouseEvent) 414 | Quartz.CGEventSetType(mouseEvent, up) 415 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, mouseEvent) 416 | for i in range(0, num-1): 417 | Quartz.CGEventSetType(mouseEvent, down) 418 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, mouseEvent) 419 | Quartz.CGEventSetType(mouseEvent, up) 420 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, mouseEvent) 421 | 422 | 423 | def _sendMouseEvent(ev, x, y, button): 424 | mouseEvent = Quartz.CGEventCreateMouseEvent(None, ev, (x, y), button) 425 | Quartz.CGEventPost(Quartz.kCGHIDEventTap, mouseEvent) 426 | 427 | 428 | def _dragTo(x, y, button): 429 | if button == LEFT: 430 | _sendMouseEvent(Quartz.kCGEventLeftMouseDragged , x, y, Quartz.kCGMouseButtonLeft) 431 | elif button == MIDDLE: 432 | _sendMouseEvent(Quartz.kCGEventOtherMouseDragged , x, y, Quartz.kCGMouseButtonCenter) 433 | elif button == RIGHT: 434 | _sendMouseEvent(Quartz.kCGEventRightMouseDragged , x, y, Quartz.kCGMouseButtonRight) 435 | else: 436 | assert False, "button argument not in ('left', 'middle', 'right')" 437 | time.sleep(0.01) # needed to allow OS time to catch up. 438 | 439 | def _moveTo(x, y): 440 | _sendMouseEvent(Quartz.kCGEventMouseMoved, x, y, 0) 441 | time.sleep(0.01) # needed to allow OS time to catch up. 442 | 443 | -------------------------------------------------------------------------------- /pyautogui/_pyautogui_x11.py: -------------------------------------------------------------------------------- 1 | # NOTE - It is a known issue that the keyboard-related functions don't work on Ubuntu VMs in Virtualbox. 2 | 3 | import pyautogui 4 | import sys 5 | import os 6 | from pyautogui import LEFT, MIDDLE, RIGHT 7 | 8 | from Xlib.display import Display 9 | from Xlib import X 10 | from Xlib.ext.xtest import fake_input 11 | import Xlib.XK 12 | 13 | BUTTON_NAME_MAPPING = {LEFT: 1, MIDDLE: 2, RIGHT: 3, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7} 14 | 15 | MOUSE_EVENT = {1: 'left', 2: 'middle', 3: 'right'} 16 | 17 | if sys.platform in ('java', 'darwin', 'win32'): 18 | raise Exception('The pyautogui_x11 module should only be loaded on a Unix system that supports X11.') 19 | 20 | #from pyautogui import * 21 | 22 | """ 23 | Much of this code is based on information gleaned from Paul Barton's PyKeyboard in PyUserInput from 2013, itself derived from Akkana Peck's pykey in 2008 ( http://www.shallowsky.com/software/crikey/pykey-0.1 ), itself derived from her "Crikey" lib. 24 | """ 25 | 26 | def _position(): 27 | """Returns the current xy coordinates of the mouse cursor as a two-integer 28 | tuple. 29 | 30 | Returns: 31 | (x, y) tuple of the current xy coordinates of the mouse cursor. 32 | """ 33 | coord = _display.screen().root.query_pointer()._data 34 | return coord["root_x"], coord["root_y"] 35 | 36 | 37 | def _size(): 38 | return _display.screen().width_in_pixels, _display.screen().height_in_pixels 39 | 40 | 41 | 42 | def _vscroll(clicks, x=None, y=None): 43 | clicks = int(clicks) 44 | if clicks == 0: 45 | return 46 | elif clicks > 0: 47 | button = 4 # scroll up 48 | else: 49 | button = 5 # scroll down 50 | 51 | for i in range(abs(clicks)): 52 | _click(x, y, button=button) 53 | 54 | 55 | def _hscroll(clicks, x=None, y=None): 56 | clicks = int(clicks) 57 | if clicks == 0: 58 | return 59 | elif clicks > 0: 60 | button = 7 # scroll right 61 | else: 62 | button = 6 # scroll left 63 | 64 | for i in range(abs(clicks)): 65 | _click(x, y, button=button) 66 | 67 | 68 | def _scroll(clicks, x=None, y=None): 69 | return _vscroll(clicks, x, y) 70 | 71 | 72 | def _click(x, y, button): 73 | assert button in BUTTON_NAME_MAPPING.keys(), "button argument not in ('left', 'middle', 'right', 4, 5, 6, 7)" 74 | button = BUTTON_NAME_MAPPING[button] 75 | 76 | _mouseDown(x, y, button) 77 | _mouseUp(x, y, button) 78 | 79 | 80 | def _moveTo(x, y): 81 | fake_input(_display, X.MotionNotify, x=x, y=y) 82 | _display.sync() 83 | 84 | 85 | def _mouseDown(x, y, button): 86 | _moveTo(x, y) 87 | assert button in BUTTON_NAME_MAPPING.keys(), "button argument not in ('left', 'middle', 'right', 4, 5, 6, 7)" 88 | button = BUTTON_NAME_MAPPING[button] 89 | fake_input(_display, X.ButtonPress, button) 90 | _display.sync() 91 | 92 | 93 | def _mouseUp(x, y, button): 94 | _moveTo(x, y) 95 | assert button in BUTTON_NAME_MAPPING.keys(), "button argument not in ('left', 'middle', 'right', 4, 5, 6, 7)" 96 | button = BUTTON_NAME_MAPPING[button] 97 | fake_input(_display, X.ButtonRelease, button) 98 | _display.sync() 99 | 100 | 101 | def _getMouseEvent(): 102 | screen = _display.screen() 103 | window = screen.root 104 | window.grab_pointer(1, X.PointerMotionMask|X.ButtonReleaseMask|X.ButtonPressMask, X.GrabModeAsync, X.GrabModeAsync, X.NONE, X.NONE, X.CurrentTime) 105 | e = _display.next_event() 106 | _display.ungrab_pointer(X.CurrentTime) 107 | 108 | if e.type == X.ButtonPress: 109 | e = MOUSE_EVENT[e.detail] + '_down' 110 | elif e.type == X.ButtonRelease: 111 | e = MOUSE_EVENT[e.detail] + '_up' 112 | else: 113 | e = 'moving' 114 | return e 115 | 116 | 117 | def _waitForMouseEvent(e): 118 | assert e in ['left_down', 'right_down', 'middle_down', 'left_up', 'middle_up', 'right_up', 'moving'], "Event can only be 'left_down', 'right_down', 'middle_down', 'left_up', 'middle_up', 'right_up', 'moving'" 119 | 120 | while(True): 121 | ee = _getMouseEvent() 122 | _ee = ee.split('_') 123 | 124 | if len(_ee) == 2: 125 | x, y = _position() 126 | if _ee[1] == 'down': 127 | _mouseDown(x, y, _ee[0]) 128 | else: 129 | _mouseUp(x, y, _ee[0]) 130 | 131 | if ee == e: 132 | return True 133 | 134 | 135 | def _keyDown(key): 136 | """Performs a keyboard key press without the release. This will put that 137 | key in a held down state. 138 | 139 | NOTE: For some reason, this does not seem to cause key repeats like would 140 | happen if a keyboard key was held down on a text field. 141 | 142 | Args: 143 | key (str): The key to be pressed down. The valid names are listed in 144 | pyautogui.KEY_NAMES. 145 | 146 | Returns: 147 | None 148 | """ 149 | if key not in keyboardMapping or keyboardMapping[key] is None: 150 | return 151 | 152 | if type(key) == int: 153 | fake_input(_display, X.KeyPress, key) 154 | _display.sync() 155 | return 156 | 157 | needsShift = pyautogui.isShiftCharacter(key) 158 | if needsShift: 159 | fake_input(_display, X.KeyPress, keyboardMapping['shift']) 160 | 161 | fake_input(_display, X.KeyPress, keyboardMapping[key]) 162 | 163 | if needsShift: 164 | fake_input(_display, X.KeyRelease, keyboardMapping['shift']) 165 | _display.sync() 166 | 167 | 168 | def _keyUp(key): 169 | """Performs a keyboard key release (without the press down beforehand). 170 | 171 | Args: 172 | key (str): The key to be released up. The valid names are listed in 173 | pyautogui.KEY_NAMES. 174 | 175 | Returns: 176 | None 177 | """ 178 | 179 | """ 180 | Release a given character key. Also works with character keycodes as 181 | integers, but not keysyms. 182 | """ 183 | if key not in keyboardMapping or keyboardMapping[key] is None: 184 | return 185 | 186 | if type(key) == int: 187 | keycode = key 188 | else: 189 | keycode = keyboardMapping[key] 190 | 191 | fake_input(_display, X.KeyRelease, keycode) 192 | _display.sync() 193 | 194 | 195 | # Taken from PyKeyboard's ctor function. 196 | _display = Display(os.environ['DISPLAY']) 197 | 198 | 199 | """ Information for keyboardMapping derived from PyKeyboard's special_key_assignment() function. 200 | 201 | The *KB dictionaries in pyautogui map a string that can be passed to keyDown(), 202 | keyUp(), or press() into the code used for the OS-specific keyboard function. 203 | 204 | They should always be lowercase, and the same keys should be used across all OSes.""" 205 | keyboardMapping = dict([(key, None) for key in pyautogui.KEY_NAMES]) 206 | keyboardMapping.update({ 207 | 'backspace': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('BackSpace')), 208 | '\b': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('BackSpace')), 209 | 'tab': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Tab')), 210 | 'enter': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Return')), 211 | 'return': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Return')), 212 | 'shift': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Shift_L')), 213 | 'ctrl': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Control_L')), 214 | 'alt': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Alt_L')), 215 | 'pause': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Pause')), 216 | 'capslock': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Caps_Lock')), 217 | 'esc': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Escape')), 218 | 'escape': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Escape')), 219 | 'pgup': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Page_Up')), 220 | 'pgdn': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Page_Down')), 221 | 'pageup': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Page_Up')), 222 | 'pagedown': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Page_Down')), 223 | 'end': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('End')), 224 | 'home': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Home')), 225 | 'left': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Left')), 226 | 'up': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Up')), 227 | 'right': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Right')), 228 | 'down': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Down')), 229 | 'select': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Select')), 230 | 'print': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Print')), 231 | 'execute': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Execute')), 232 | 'prtsc': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Print')), 233 | 'prtscr': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Print')), 234 | 'prntscrn': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Print')), 235 | 'printscreen': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Print')), 236 | 'insert': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Insert')), 237 | 'del': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Delete')), 238 | 'delete': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Delete')), 239 | 'help': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Help')), 240 | 'winleft': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Super_L')), 241 | 'winright': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Super_R')), 242 | 'apps': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Menu')), 243 | 'num0': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_0')), 244 | 'num1': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_1')), 245 | 'num2': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_2')), 246 | 'num3': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_3')), 247 | 'num4': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_4')), 248 | 'num5': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_5')), 249 | 'num6': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_6')), 250 | 'num7': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_7')), 251 | 'num8': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_8')), 252 | 'num9': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_9')), 253 | 'multiply': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_Multiply')), 254 | 'add': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_Add')), 255 | 'separator': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_Separator')), 256 | 'subtract': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_Subtract')), 257 | 'decimal': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_Decimal')), 258 | 'divide': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('KP_Divide')), 259 | 'f1': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F1')), 260 | 'f2': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F2')), 261 | 'f3': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F3')), 262 | 'f4': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F4')), 263 | 'f5': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F5')), 264 | 'f6': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F6')), 265 | 'f7': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F7')), 266 | 'f8': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F8')), 267 | 'f9': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F9')), 268 | 'f10': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F10')), 269 | 'f11': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F11')), 270 | 'f12': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F12')), 271 | 'f13': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F13')), 272 | 'f14': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F14')), 273 | 'f15': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F15')), 274 | 'f16': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F16')), 275 | 'f17': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F17')), 276 | 'f18': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F18')), 277 | 'f19': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F19')), 278 | 'f20': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F20')), 279 | 'f21': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F21')), 280 | 'f22': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F22')), 281 | 'f23': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F23')), 282 | 'f24': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('F24')), 283 | 'numlock': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Num_Lock')), 284 | 'scrolllock': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Scroll_Lock')), 285 | 'shiftleft': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Shift_L')), 286 | 'shiftright': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Shift_R')), 287 | 'ctrlleft': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Control_L')), 288 | 'ctrlright': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Control_R')), 289 | 'altleft': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Alt_L')), 290 | 'altright': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Alt_R')), 291 | # These are added because unlike a-zA-Z0-9, the single characters do not have a 292 | ' ': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('space')), 293 | 'space': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('space')), 294 | '\t': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Tab')), 295 | '\n': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Return')), # for some reason this needs to be cr, not lf 296 | '\r': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Return')), 297 | '\e': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('Escape')), 298 | '!': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('exclam')), 299 | '#': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('numbersign')), 300 | '%': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('percent')), 301 | '$': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('dollar')), 302 | '&': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('ampersand')), 303 | '"': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('quotedbl')), 304 | "'": _display.keysym_to_keycode(Xlib.XK.string_to_keysym('apostrophe')), 305 | '(': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('parenleft')), 306 | ')': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('parenright')), 307 | '*': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('asterisk')), 308 | '=': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('equal')), 309 | '+': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('plus')), 310 | ',': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('comma')), 311 | '-': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('minus')), 312 | '.': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('period')), 313 | '/': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('slash')), 314 | ':': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('colon')), 315 | ';': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('semicolon')), 316 | '<': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('less')), 317 | '>': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('greater')), 318 | '?': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('question')), 319 | '@': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('at')), 320 | '[': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('bracketleft')), 321 | ']': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('bracketright')), 322 | '\\': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('backslash')), 323 | '^': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('asciicircum')), 324 | '_': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('underscore')), 325 | '`': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('grave')), 326 | '{': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('braceleft')), 327 | '|': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('bar')), 328 | '}': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('braceright')), 329 | '~': _display.keysym_to_keycode(Xlib.XK.string_to_keysym('asciitilde')), 330 | }) 331 | 332 | # Trading memory for time" populate winKB so we don't have to call VkKeyScanA each time. 333 | for c in """abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890""": 334 | keyboardMapping[c] = _display.keysym_to_keycode(Xlib.XK.string_to_keysym(c)) 335 | -------------------------------------------------------------------------------- /pyautogui/_pyautogui_win.py: -------------------------------------------------------------------------------- 1 | # Windows implementation of PyAutoGUI functions. 2 | # BSD license 3 | # Al Sweigart al@inventwithpython.com 4 | 5 | import ctypes 6 | import ctypes.wintypes 7 | import pyautogui 8 | from pyautogui import LEFT, MIDDLE, RIGHT 9 | 10 | import sys 11 | if sys.platform != 'win32': 12 | raise Exception('The pyautogui_win module should only be loaded on a Windows system.') 13 | 14 | 15 | # Fixes the scaling issues where PyAutoGUI was reporting the wrong resolution: 16 | try: 17 | ctypes.windll.user32.SetProcessDPIAware() 18 | except AttributeError: 19 | pass # Windows XP doesn't support this, so just do nothing. 20 | 21 | 22 | """ 23 | A lot of this code is probably repeated from win32 extensions module, but I didn't want to have that dependency. 24 | 25 | Note: According to http://msdn.microsoft.com/en-us/library/windows/desktop/ms646260(v=vs.85).aspx 26 | the ctypes.windll.user32.mouse_event() function has been superceded by SendInput. 27 | 28 | SendInput() is documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/ms646310(v=vs.85).aspx 29 | 30 | UPDATE: SendInput() doesn't seem to be working for me. I've switched back to mouse_event().""" 31 | 32 | 33 | # Event codes to be passed to the mouse_event() win32 function. 34 | # Documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/ms646273(v=vs.85).aspx 35 | MOUSEEVENTF_MOVE = 0x0001 36 | MOUSEEVENTF_LEFTDOWN = 0x0002 37 | MOUSEEVENTF_LEFTUP = 0x0004 38 | MOUSEEVENTF_LEFTCLICK = MOUSEEVENTF_LEFTDOWN + MOUSEEVENTF_LEFTUP 39 | MOUSEEVENTF_RIGHTDOWN = 0x0008 40 | MOUSEEVENTF_RIGHTUP = 0x0010 41 | MOUSEEVENTF_RIGHTCLICK = MOUSEEVENTF_RIGHTDOWN + MOUSEEVENTF_RIGHTUP 42 | MOUSEEVENTF_MIDDLEDOWN = 0x0020 43 | MOUSEEVENTF_MIDDLEUP = 0x0040 44 | MOUSEEVENTF_MIDDLECLICK = MOUSEEVENTF_MIDDLEDOWN + MOUSEEVENTF_MIDDLEUP 45 | 46 | MOUSEEVENTF_ABSOLUTE = 0x8000 47 | MOUSEEVENTF_WHEEL = 0x0800 48 | MOUSEEVENTF_HWHEEL = 0x01000 49 | 50 | # Documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/ms646304(v=vs.85).aspx 51 | KEYEVENTF_KEYDOWN = 0x0000 # Technically this constant doesn't exist in the MS documentation. It's the lack of KEYEVENTF_KEYUP that means pressing the key down. 52 | KEYEVENTF_KEYUP = 0x0002 53 | 54 | # Documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/ms646270(v=vs.85).aspx 55 | INPUT_MOUSE = 0 56 | INPUT_KEYBOARD = 1 57 | 58 | 59 | # This ctypes structure is for a Win32 POINT structure, 60 | # which is documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx 61 | # The POINT structure is used by GetCursorPos(). 62 | class POINT(ctypes.Structure): 63 | _fields_ = [("x", ctypes.c_long), 64 | ("y", ctypes.c_long)] 65 | 66 | # These ctypes structures are for Win32 INPUT, MOUSEINPUT, KEYBDINPUT, and HARDWAREINPUT structures, 67 | # used by SendInput and documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/ms646270(v=vs.85).aspx 68 | # Thanks to BSH for this StackOverflow answer: https://stackoverflow.com/questions/18566289/how-would-you-recreate-this-windows-api-structure-with-ctypes 69 | class MOUSEINPUT(ctypes.Structure): 70 | _fields_ = [ 71 | ('dx', ctypes.wintypes.LONG), 72 | ('dy', ctypes.wintypes.LONG), 73 | ('mouseData', ctypes.wintypes.DWORD), 74 | ('dwFlags', ctypes.wintypes.DWORD), 75 | ('time', ctypes.wintypes.DWORD), 76 | ('dwExtraInfo', ctypes.POINTER(ctypes.wintypes.ULONG)), 77 | ] 78 | 79 | class KEYBDINPUT(ctypes.Structure): 80 | _fields_ = [ 81 | ('wVk', ctypes.wintypes.WORD), 82 | ('wScan', ctypes.wintypes.WORD), 83 | ('dwFlags', ctypes.wintypes.DWORD), 84 | ('time', ctypes.wintypes.DWORD), 85 | ('dwExtraInfo', ctypes.POINTER(ctypes.wintypes.ULONG)), 86 | ] 87 | 88 | class HARDWAREINPUT(ctypes.Structure): 89 | _fields_ = [ 90 | ('uMsg', ctypes.wintypes.DWORD), 91 | ('wParamL', ctypes.wintypes.WORD), 92 | ('wParamH', ctypes.wintypes.DWORD) 93 | ] 94 | 95 | class INPUT(ctypes.Structure): 96 | class _I(ctypes.Union): 97 | _fields_ = [ 98 | ('mi', MOUSEINPUT), 99 | ('ki', KEYBDINPUT), 100 | ('hi', HARDWAREINPUT), 101 | ] 102 | 103 | _anonymous_ = ('i', ) 104 | _fields_ = [ 105 | ('type', ctypes.wintypes.DWORD), 106 | ('i', _I), 107 | ] 108 | # End of the SendInput win32 data structures. 109 | 110 | 111 | 112 | """ Keyboard key mapping for pyautogui: 113 | Documented at http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx 114 | 115 | The *KB dictionaries in pyautogui map a string that can be passed to keyDown(), 116 | keyUp(), or press() into the code used for the OS-specific keyboard function. 117 | 118 | They should always be lowercase, and the same keys should be used across all OSes.""" 119 | keyboardMapping = dict([(key, None) for key in pyautogui.KEY_NAMES]) 120 | keyboardMapping.update({ 121 | 'backspace': 0x08, # VK_BACK 122 | '\b': 0x08, # VK_BACK 123 | 'super': 0x5B, #VK_LWIN 124 | 'tab': 0x09, # VK_TAB 125 | '\t': 0x09, # VK_TAB 126 | 'clear': 0x0c, # VK_CLEAR 127 | 'enter': 0x0d, # VK_RETURN 128 | '\n': 0x0d, # VK_RETURN 129 | 'return': 0x0d, # VK_RETURN 130 | 'shift': 0x10, # VK_SHIFT 131 | 'ctrl': 0x11, # VK_CONTROL 132 | 'alt': 0x12, # VK_MENU 133 | 'pause': 0x13, # VK_PAUSE 134 | 'capslock': 0x14, # VK_CAPITAL 135 | 'kana': 0x15, # VK_KANA 136 | 'hanguel': 0x15, # VK_HANGUEL 137 | 'hangul': 0x15, # VK_HANGUL 138 | 'junja': 0x17, # VK_JUNJA 139 | 'final': 0x18, # VK_FINAL 140 | 'hanja': 0x19, # VK_HANJA 141 | 'kanji': 0x19, # VK_KANJI 142 | 'esc': 0x1b, # VK_ESCAPE 143 | 'escape': 0x1b, # VK_ESCAPE 144 | 'convert': 0x1c, # VK_CONVERT 145 | 'nonconvert': 0x1d, # VK_NONCONVERT 146 | 'accept': 0x1e, # VK_ACCEPT 147 | 'modechange': 0x1f, # VK_MODECHANGE 148 | ' ': 0x20, # VK_SPACE 149 | 'space': 0x20, # VK_SPACE 150 | 'pgup': 0x21, # VK_PRIOR 151 | 'pgdn': 0x22, # VK_NEXT 152 | 'pageup': 0x21, # VK_PRIOR 153 | 'pagedown': 0x22, # VK_NEXT 154 | 'end': 0x23, # VK_END 155 | 'home': 0x24, # VK_HOME 156 | 'left': 0x25, # VK_LEFT 157 | 'up': 0x26, # VK_UP 158 | 'right': 0x27, # VK_RIGHT 159 | 'down': 0x28, # VK_DOWN 160 | 'select': 0x29, # VK_SELECT 161 | 'print': 0x2a, # VK_PRINT 162 | 'execute': 0x2b, # VK_EXECUTE 163 | 'prtsc': 0x2c, # VK_SNAPSHOT 164 | 'prtscr': 0x2c, # VK_SNAPSHOT 165 | 'prntscrn': 0x2c, # VK_SNAPSHOT 166 | 'printscreen': 0x2c, # VK_SNAPSHOT 167 | 'insert': 0x2d, # VK_INSERT 168 | 'del': 0x2e, # VK_DELETE 169 | 'delete': 0x2e, # VK_DELETE 170 | 'help': 0x2f, # VK_HELP 171 | 'win': 0x5b, # VK_LWIN 172 | 'winleft': 0x5b, # VK_LWIN 173 | 'winright': 0x5c, # VK_RWIN 174 | 'apps': 0x5d, # VK_APPS 175 | 'sleep': 0x5f, # VK_SLEEP 176 | 'num0': 0x60, # VK_NUMPAD0 177 | 'num1': 0x61, # VK_NUMPAD1 178 | 'num2': 0x62, # VK_NUMPAD2 179 | 'num3': 0x63, # VK_NUMPAD3 180 | 'num4': 0x64, # VK_NUMPAD4 181 | 'num5': 0x65, # VK_NUMPAD5 182 | 'num6': 0x66, # VK_NUMPAD6 183 | 'num7': 0x67, # VK_NUMPAD7 184 | 'num8': 0x68, # VK_NUMPAD8 185 | 'num9': 0x69, # VK_NUMPAD9 186 | 'multiply': 0x6a, # VK_MULTIPLY ??? Is this the numpad *? 187 | 'add': 0x6b, # VK_ADD ??? Is this the numpad +? 188 | 'separator': 0x6c, # VK_SEPARATOR ??? Is this the numpad enter? 189 | 'subtract': 0x6d, # VK_SUBTRACT ??? Is this the numpad -? 190 | 'decimal': 0x6e, # VK_DECIMAL 191 | 'divide': 0x6f, # VK_DIVIDE 192 | 'f1': 0x70, # VK_F1 193 | 'f2': 0x71, # VK_F2 194 | 'f3': 0x72, # VK_F3 195 | 'f4': 0x73, # VK_F4 196 | 'f5': 0x74, # VK_F5 197 | 'f6': 0x75, # VK_F6 198 | 'f7': 0x76, # VK_F7 199 | 'f8': 0x77, # VK_F8 200 | 'f9': 0x78, # VK_F9 201 | 'f10': 0x79, # VK_F10 202 | 'f11': 0x7a, # VK_F11 203 | 'f12': 0x7b, # VK_F12 204 | 'f13': 0x7c, # VK_F13 205 | 'f14': 0x7d, # VK_F14 206 | 'f15': 0x7e, # VK_F15 207 | 'f16': 0x7f, # VK_F16 208 | 'f17': 0x80, # VK_F17 209 | 'f18': 0x81, # VK_F18 210 | 'f19': 0x82, # VK_F19 211 | 'f20': 0x83, # VK_F20 212 | 'f21': 0x84, # VK_F21 213 | 'f22': 0x85, # VK_F22 214 | 'f23': 0x86, # VK_F23 215 | 'f24': 0x87, # VK_F24 216 | 'numlock': 0x90, # VK_NUMLOCK 217 | 'scrolllock': 0x91, # VK_SCROLL 218 | 'shiftleft': 0xa0, # VK_LSHIFT 219 | 'shiftright': 0xa1, # VK_RSHIFT 220 | 'ctrlleft': 0xa2, # VK_LCONTROL 221 | 'ctrlright': 0xa3, # VK_RCONTROL 222 | 'altleft': 0xa4, # VK_LMENU 223 | 'altright': 0xa5, # VK_RMENU 224 | 'browserback': 0xa6, # VK_BROWSER_BACK 225 | 'browserforward': 0xa7, # VK_BROWSER_FORWARD 226 | 'browserrefresh': 0xa8, # VK_BROWSER_REFRESH 227 | 'browserstop': 0xa9, # VK_BROWSER_STOP 228 | 'browsersearch': 0xaa, # VK_BROWSER_SEARCH 229 | 'browserfavorites': 0xab, # VK_BROWSER_FAVORITES 230 | 'browserhome': 0xac, # VK_BROWSER_HOME 231 | 'volumemute': 0xad, # VK_VOLUME_MUTE 232 | 'volumedown': 0xae, # VK_VOLUME_DOWN 233 | 'volumeup': 0xaf, # VK_VOLUME_UP 234 | 'nexttrack': 0xb0, # VK_MEDIA_NEXT_TRACK 235 | 'prevtrack': 0xb1, # VK_MEDIA_PREV_TRACK 236 | 'stop': 0xb2, # VK_MEDIA_STOP 237 | 'playpause': 0xb3, # VK_MEDIA_PLAY_PAUSE 238 | 'launchmail': 0xb4, # VK_LAUNCH_MAIL 239 | 'launchmediaselect': 0xb5, # VK_LAUNCH_MEDIA_SELECT 240 | 'launchapp1': 0xb6, # VK_LAUNCH_APP1 241 | 'launchapp2': 0xb7, # VK_LAUNCH_APP2 242 | }) 243 | 244 | # There are other virtual key constants that are not used here because the printable ascii keys are 245 | # handled in the following `for` loop. 246 | # The virtual key constants that aren't used are: 247 | # VK_OEM_1, VK_OEM_PLUS, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_2, VK_OEM_3, VK_OEM_4, 248 | # VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_PACKET, VK_ATTN, VK_CRSEL, VK_EXSEL, VK_EREOF, 249 | # VK_PLAY, VK_ZOOM, VK_NONAME, VK_PA1, VK_OEM_CLEAR 250 | 251 | # Populate the basic printable ascii characters. 252 | # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscana 253 | for c in range(32, 128): 254 | keyboardMapping[chr(c)] = ctypes.windll.user32.VkKeyScanA(ctypes.wintypes.WCHAR(chr(c))) 255 | 256 | 257 | def _keyDown(key): 258 | """Performs a keyboard key press without the release. This will put that 259 | key in a held down state. 260 | 261 | NOTE: For some reason, this does not seem to cause key repeats like would 262 | happen if a keyboard key was held down on a text field. 263 | 264 | Args: 265 | key (str): The key to be pressed down. The valid names are listed in 266 | pyautogui.KEY_NAMES. 267 | 268 | Returns: 269 | None 270 | """ 271 | if key not in keyboardMapping or keyboardMapping[key] is None: 272 | return 273 | 274 | needsShift = pyautogui.isShiftCharacter(key) 275 | 276 | """ 277 | # OLD CODE: The new code relies on having all keys be loaded in keyboardMapping from the start. 278 | if key in keyboardMapping.keys(): 279 | vkCode = keyboardMapping[key] 280 | elif len(key) == 1: 281 | # note: I could use this case to update keyboardMapping to cache the VkKeyScan results, but I've decided not to just to make any possible bugs easier to reproduce. 282 | vkCode = ctypes.windll.user32.VkKeyScanW(ctypes.wintypes.WCHAR(key)) 283 | if vkCode == -1: 284 | raise ValueError('There is no VK code for key "%s"' % (key)) 285 | if vkCode > 0x100: # the vk code will be > 0x100 if it needs shift 286 | vkCode -= 0x100 287 | needsShift = True 288 | """ 289 | mods, vkCode = divmod(keyboardMapping[key], 0x100) 290 | 291 | for apply_mod, vk_mod in [(mods & 4, 0x12), (mods & 2, 0x11), 292 | (mods & 1 or needsShift, 0x10)]: #HANKAKU not suported! mods & 8 293 | if apply_mod: 294 | ctypes.windll.user32.keybd_event(vk_mod, 0, KEYEVENTF_KEYDOWN, 0) # 295 | ctypes.windll.user32.keybd_event(vkCode, 0, KEYEVENTF_KEYDOWN, 0) 296 | for apply_mod, vk_mod in [(mods & 1 or needsShift, 0x10), (mods & 2, 0x11), 297 | (mods & 4, 0x12)]: #HANKAKU not suported! mods & 8 298 | if apply_mod: 299 | ctypes.windll.user32.keybd_event(vk_mod, 0, KEYEVENTF_KEYUP, 0) # 300 | 301 | 302 | def _keyUp(key): 303 | """Performs a keyboard key release (without the press down beforehand). 304 | 305 | Args: 306 | key (str): The key to be released up. The valid names are listed in 307 | pyautogui.KEY_NAMES. 308 | 309 | Returns: 310 | None 311 | """ 312 | if key not in keyboardMapping or keyboardMapping[key] is None: 313 | return 314 | 315 | needsShift = pyautogui.isShiftCharacter(key) 316 | """ 317 | # OLD CODE: The new code relies on having all keys be loaded in keyboardMapping from the start. 318 | if key in keyboardMapping.keys(): 319 | vkCode = keyboardMapping[key] 320 | elif len(key) == 1: 321 | # note: I could use this case to update keyboardMapping to cache the VkKeyScan results, but I've decided not to just to make any possible bugs easier to reproduce. 322 | vkCode = ctypes.windll.user32.VkKeyScanW(ctypes.wintypes.WCHAR(key)) 323 | if vkCode == -1: 324 | raise ValueError('There is no VK code for key "%s"' % (key)) 325 | if vkCode > 0x100: # the vk code will be > 0x100 if it needs shift 326 | vkCode -= 0x100 327 | needsShift = True 328 | """ 329 | mods, vkCode = divmod(keyboardMapping[key], 0x100) 330 | 331 | for apply_mod, vk_mod in [(mods & 4, 0x12), (mods & 2, 0x11), 332 | (mods & 1 or needsShift, 0x10)]: #HANKAKU not suported! mods & 8 333 | if apply_mod: 334 | ctypes.windll.user32.keybd_event(vk_mod, 0, 0, 0) # 335 | ctypes.windll.user32.keybd_event(vkCode, 0, KEYEVENTF_KEYUP, 0) 336 | for apply_mod, vk_mod in [(mods & 1 or needsShift, 0x10), (mods & 2, 0x11), 337 | (mods & 4, 0x12)]: #HANKAKU not suported! mods & 8 338 | if apply_mod: 339 | ctypes.windll.user32.keybd_event(vk_mod, 0, KEYEVENTF_KEYUP, 0) # 340 | 341 | 342 | def _position(): 343 | """Returns the current xy coordinates of the mouse cursor as a two-integer 344 | tuple by calling the GetCursorPos() win32 function. 345 | 346 | Returns: 347 | (x, y) tuple of the current xy coordinates of the mouse cursor. 348 | """ 349 | 350 | cursor = POINT() 351 | ctypes.windll.user32.GetCursorPos(ctypes.byref(cursor)) 352 | return (cursor.x, cursor.y) 353 | 354 | 355 | def _size(): 356 | """Returns the width and height of the screen as a two-integer tuple. 357 | 358 | Returns: 359 | (width, height) tuple of the screen size, in pixels. 360 | """ 361 | return (ctypes.windll.user32.GetSystemMetrics(0), ctypes.windll.user32.GetSystemMetrics(1)) 362 | 363 | 364 | def _moveTo(x, y): 365 | """Send the mouse move event to Windows by calling SetCursorPos() win32 366 | function. 367 | 368 | Args: 369 | button (str): The mouse button, either 'left', 'middle', or 'right' 370 | x (int): The x position of the mouse event. 371 | y (int): The y position of the mouse event. 372 | 373 | Returns: 374 | None 375 | """ 376 | ctypes.windll.user32.SetCursorPos(x, y) 377 | # This was a possible solution to issue #314 https://github.com/asweigart/pyautogui/issues/314 378 | # but I'd like to hang on to SetCursorPos because mouse_event() has been superceded. 379 | #_sendMouseEvent(MOUSEEVENTF_MOVE + MOUSEEVENTF_ABSOLUTE, x, y) 380 | 381 | 382 | def _mouseDown(x, y, button): 383 | """Send the mouse down event to Windows by calling the mouse_event() win32 384 | function. 385 | 386 | Args: 387 | x (int): The x position of the mouse event. 388 | y (int): The y position of the mouse event. 389 | button (str): The mouse button, either 'left', 'middle', or 'right' 390 | 391 | Returns: 392 | None 393 | """ 394 | if button not in (LEFT, MIDDLE, RIGHT): 395 | raise ValueError('button arg to _click() must be one of "left", "middle", or "right", not %s' % button) 396 | 397 | if button == LEFT: 398 | EV = MOUSEEVENTF_LEFTDOWN 399 | elif button == MIDDLE: 400 | EV = MOUSEEVENTF_MIDDLEDOWN 401 | elif button == RIGHT: 402 | EV = MOUSEEVENTF_RIGHTDOWN 403 | 404 | try: 405 | _sendMouseEvent(EV, x, y) 406 | except (PermissionError, OSError): 407 | # TODO: We need to figure out how to prevent these errors, see https://github.com/asweigart/pyautogui/issues/60 408 | pass 409 | 410 | 411 | def _mouseUp(x, y, button): 412 | """Send the mouse up event to Windows by calling the mouse_event() win32 413 | function. 414 | 415 | Args: 416 | x (int): The x position of the mouse event. 417 | y (int): The y position of the mouse event. 418 | button (str): The mouse button, either 'left', 'middle', or 'right' 419 | 420 | Returns: 421 | None 422 | """ 423 | if button not in (LEFT, MIDDLE, RIGHT): 424 | raise ValueError('button arg to _click() must be one of "left", "middle", or "right", not %s' % button) 425 | 426 | if button == LEFT: 427 | EV = MOUSEEVENTF_LEFTUP 428 | elif button == MIDDLE: 429 | EV = MOUSEEVENTF_MIDDLEUP 430 | elif button == RIGHT: 431 | EV = MOUSEEVENTF_RIGHTUP 432 | 433 | try: 434 | _sendMouseEvent(EV, x, y) 435 | except (PermissionError, OSError): # TODO: We need to figure out how to prevent these errors, see https://github.com/asweigart/pyautogui/issues/60 436 | pass 437 | 438 | 439 | def _click(x, y, button): 440 | """Send the mouse click event to Windows by calling the mouse_event() win32 441 | function. 442 | 443 | Args: 444 | button (str): The mouse button, either 'left', 'middle', or 'right' 445 | x (int): The x position of the mouse event. 446 | y (int): The y position of the mouse event. 447 | 448 | Returns: 449 | None 450 | """ 451 | if button not in (LEFT, MIDDLE, RIGHT): 452 | raise ValueError('button arg to _click() must be one of "left", "middle", or "right", not %s' % button) 453 | 454 | if button == LEFT: 455 | EV = MOUSEEVENTF_LEFTCLICK 456 | elif button == MIDDLE: 457 | EV = MOUSEEVENTF_MIDDLECLICK 458 | elif button ==RIGHT: 459 | EV = MOUSEEVENTF_RIGHTCLICK 460 | 461 | try: 462 | _sendMouseEvent(EV, x, y) 463 | except (PermissionError, OSError): 464 | # TODO: We need to figure out how to prevent these errors, see https://github.com/asweigart/pyautogui/issues/60 465 | pass 466 | 467 | 468 | def _sendMouseEvent(ev, x, y, dwData=0): 469 | """The helper function that actually makes the call to the mouse_event() 470 | win32 function. 471 | 472 | Args: 473 | ev (int): The win32 code for the mouse event. Use one of the MOUSEEVENTF_* 474 | constants for this argument. 475 | x (int): The x position of the mouse event. 476 | y (int): The y position of the mouse event. 477 | dwData (int): The argument for mouse_event()'s dwData parameter. So far 478 | this is only used by mouse scrolling. 479 | 480 | Returns: 481 | None 482 | """ 483 | assert x != None and y != None, 'x and y cannot be set to None' 484 | # TODO: ARG! For some reason, SendInput isn't working for mouse events. I'm switching to using the older mouse_event win32 function. 485 | #mouseStruct = MOUSEINPUT() 486 | #mouseStruct.dx = x 487 | #mouseStruct.dy = y 488 | #mouseStruct.mouseData = ev 489 | #mouseStruct.time = 0 490 | #mouseStruct.dwExtraInfo = ctypes.pointer(ctypes.c_ulong(0)) # according to https://stackoverflow.com/questions/13564851/generate-keyboard-events I can just set this. I don't really care about this value. 491 | #inputStruct = INPUT() 492 | #inputStruct.mi = mouseStruct 493 | #inputStruct.type = INPUT_MOUSE 494 | #ctypes.windll.user32.SendInput(1, ctypes.pointer(inputStruct), ctypes.sizeof(inputStruct)) 495 | 496 | # TODO Note: We need to handle additional buttons, which I believe is documented here: 497 | # https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-mouse_event 498 | 499 | width, height = _size() 500 | convertedX = 65536 * x // width + 1 501 | convertedY = 65536 * y // height + 1 502 | ctypes.windll.user32.mouse_event(ev, ctypes.c_long(convertedX), ctypes.c_long(convertedY), dwData, 0) 503 | 504 | # TODO: Too many false positives with this code: See: https://github.com/asweigart/pyautogui/issues/108 505 | #if ctypes.windll.kernel32.GetLastError() != 0: 506 | # raise ctypes.WinError() 507 | 508 | 509 | def _scroll(clicks, x=None, y=None): 510 | """Send the mouse vertical scroll event to Windows by calling the 511 | mouse_event() win32 function. 512 | 513 | Args: 514 | clicks (int): The amount of scrolling to do. A positive value is the mouse 515 | wheel moving forward (scrolling up), a negative value is backwards (down). 516 | x (int): The x position of the mouse event. 517 | y (int): The y position of the mouse event. 518 | 519 | Returns: 520 | None 521 | """ 522 | startx, starty = _position() 523 | width, height = _size() 524 | 525 | if x is None: 526 | x = startx 527 | else: 528 | if x < 0: 529 | x = 0 530 | elif x >= width: 531 | x = width - 1 532 | if y is None: 533 | y = starty 534 | else: 535 | if y < 0: 536 | y = 0 537 | elif y >= height: 538 | y = height - 1 539 | 540 | try: 541 | _sendMouseEvent(MOUSEEVENTF_WHEEL, x, y, dwData=clicks) 542 | except (PermissionError, OSError): # TODO: We need to figure out how to prevent these errors, see https://github.com/asweigart/pyautogui/issues/60 543 | pass 544 | 545 | 546 | def _hscroll(clicks, x, y): 547 | """Send the mouse horizontal scroll event to Windows by calling the 548 | mouse_event() win32 function. 549 | 550 | Args: 551 | clicks (int): The amount of scrolling to do. A positive value is the mouse 552 | wheel moving right, a negative value is moving left. 553 | x (int): The x position of the mouse event. 554 | y (int): The y position of the mouse event. 555 | 556 | Returns: 557 | None 558 | """ 559 | return _scroll(clicks, x, y) 560 | 561 | 562 | def _vscroll(clicks, x, y): 563 | """A wrapper for _scroll(), which does vertical scrolling. 564 | 565 | Args: 566 | clicks (int): The amount of scrolling to do. A positive value is the mouse 567 | wheel moving forward (scrolling up), a negative value is backwards (down). 568 | x (int): The x position of the mouse event. 569 | y (int): The y position of the mouse event. 570 | 571 | Returns: 572 | None 573 | """ 574 | return _scroll(clicks, x, y) 575 | 576 | -------------------------------------------------------------------------------- /gui/custom.py: -------------------------------------------------------------------------------- 1 | # Dindo Bot 2 | # Copyright (c) 2018 - 2019 AXeL 3 | 4 | import gi 5 | gi.require_version('Gtk', '3.0') 6 | from gi.repository import Gtk, Gdk, Pango 7 | from lib.tools import fit_position_to_destination 8 | from lib.parser import parse_color 9 | import math 10 | 11 | class CustomComboBox(Gtk.ComboBoxText): 12 | 13 | def __init__(self, data_list=[], sort=False): 14 | Gtk.ComboBoxText.__init__(self) 15 | # set max chars width 16 | for renderer in self.get_cells(): 17 | renderer.props.max_width_chars = 10 18 | renderer.props.ellipsize = Pango.EllipsizeMode.END 19 | # append data 20 | self.append_list(data_list, sort) 21 | 22 | def append_list(self, data_list, sort=False, clear=False): 23 | # clear combobox 24 | if clear: 25 | self.remove_all() 26 | # sort data 27 | if sort: 28 | data_list = sorted(data_list) 29 | # append data 30 | for text in data_list: 31 | self.append_text(text) 32 | 33 | def sync_with_combo(self, combo, use_contains=False): 34 | if self.get_active() != -1 and combo.get_active() != -1: 35 | # do not allow same text at same time 36 | self_text = self.get_active_text() 37 | combo_text = combo.get_active_text() 38 | if (use_contains and (self_text in combo_text or combo_text in self_text)) or self_text == combo_text: 39 | combo.set_active(-1) 40 | 41 | class TextValueComboBox(Gtk.ComboBox): 42 | 43 | def __init__(self, data_list=[], model=None, text_key=None, value_key=None, sort_key=None): 44 | Gtk.ComboBox.__init__(self) 45 | # set max chars width 46 | renderer_text = Gtk.CellRendererText() 47 | renderer_text.props.max_width_chars = 10 48 | renderer_text.props.ellipsize = Pango.EllipsizeMode.END 49 | # append data 50 | if model is None: 51 | self.model = Gtk.ListStore(str, str) 52 | else: 53 | self.model = model 54 | self.append_list(data_list, text_key, value_key, sort_key) 55 | self.set_model(self.model) 56 | self.pack_start(renderer_text, True) 57 | self.add_attribute(renderer_text, 'text', 0) 58 | # save data list values (for further use) 59 | self.values = [ item[value_key] for item in data_list ] 60 | 61 | def append_list(self, data_list, text_key, value_key, sort_key=None, clear=False): 62 | # clear combobox 63 | if clear: 64 | self.remove_all() 65 | # sort data 66 | if sort_key is not None: 67 | data_list = sorted(data_list, key=lambda item: item[sort_key]) 68 | # append data 69 | if text_key is not None and value_key is not None: 70 | for data in data_list: 71 | self.model.append([data[text_key], data[value_key]]) 72 | 73 | def _get_active(self, index): 74 | active = self.get_active() 75 | if active != -1: 76 | return self.model[active][index] 77 | else: 78 | return None 79 | 80 | def get_active_text(self): 81 | return self._get_active(0) 82 | 83 | def get_active_value(self): 84 | return self._get_active(1) 85 | 86 | def set_active_value(self, value): 87 | self.set_active(self.values.index(value)) 88 | 89 | def remove_all(self): 90 | self.model.clear() 91 | 92 | class CustomTreeView(Gtk.Frame): 93 | 94 | def __init__(self, model, columns): 95 | Gtk.Frame.__init__(self) 96 | self.perform_scroll = False 97 | self.model = model 98 | self.columns = columns 99 | self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 100 | self.add(self.vbox) 101 | ## ScrolledWindow 102 | scrolled_window = Gtk.ScrolledWindow() 103 | self.vbox.pack_start(scrolled_window, True, True, 0) 104 | # TreeView 105 | self.tree_view = Gtk.TreeView(model) 106 | scrolled_window.add(self.tree_view) 107 | for column in columns: 108 | self.tree_view.append_column(column) 109 | self.tree_view.connect('size-allocate', self.scroll_tree_view) 110 | self.selection = self.tree_view.get_selection() 111 | self.selection.set_mode(Gtk.SelectionMode.SINGLE) 112 | 113 | def is_empty(self): 114 | return len(self.model) == 0 115 | 116 | def connect(self, event_name, event_callback): 117 | if event_name == 'selection-changed': 118 | self.selection.connect('changed', event_callback) 119 | else: 120 | self.tree_view.connect(event_name, event_callback) 121 | 122 | def append_row(self, row, select=True, scroll_to=True): 123 | # append row 124 | self.model.append(row) 125 | # scroll to row 126 | if scroll_to: 127 | self.perform_scroll = True 128 | # select row 129 | if select: 130 | index = len(self.model) - 1 131 | self.select_row(index) 132 | 133 | def select_row(self, index): 134 | path = Gtk.TreePath(index) 135 | self.selection.select_path(path) 136 | 137 | def get_row_index(self, row_iter): 138 | return self.model.get_path(row_iter).get_indices()[0] 139 | 140 | def get_rows_count(self): 141 | return len(self.model) 142 | 143 | def get_selected_row(self): 144 | model, tree_iter = self.selection.get_selected() 145 | if tree_iter: 146 | row = [] 147 | for i in range(len(self.columns)): 148 | column_value = model.get_value(tree_iter, i) 149 | row.append(column_value) 150 | return row 151 | else: 152 | return None 153 | 154 | def remove_selected_row(self): 155 | # remove selected row 156 | model, tree_iter = self.selection.get_selected() 157 | if tree_iter: 158 | model.remove(tree_iter) 159 | 160 | def scroll_tree_view(self, widget, event): 161 | if self.perform_scroll: 162 | adj = widget.get_vadjustment() 163 | adj.set_value(adj.get_upper() - adj.get_page_size()) 164 | self.perform_scroll = False 165 | 166 | class CustomListBox(Gtk.Frame): 167 | 168 | def __init__(self, parent=None, allow_moving=True): 169 | Gtk.Frame.__init__(self) 170 | self.parent = parent 171 | self.allow_moving = allow_moving 172 | self.perform_scroll = False 173 | self.add_callback = None 174 | self.delete_callback = None 175 | self.activate_callback = None 176 | ## ListBox 177 | vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 178 | self.add(vbox) 179 | self.listbox = Gtk.ListBox() 180 | self.listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) 181 | self.listbox.connect('size-allocate', self.on_size_allocate) 182 | self.listbox.connect('row-activated', self.on_row_activated) 183 | scrolled_window = Gtk.ScrolledWindow() 184 | scrolled_window.add(self.listbox) 185 | vbox.pack_start(scrolled_window, True, True, 0) 186 | ## ActionBar 187 | actionbar = Gtk.ActionBar() 188 | vbox.pack_end(actionbar, False, False, 0) 189 | default_buttons_box = ButtonBox(linked=True) 190 | actionbar.pack_start(default_buttons_box) 191 | if allow_moving: 192 | # Move up 193 | self.move_up_button = Gtk.Button() 194 | self.move_up_button.set_tooltip_text('Move up') 195 | self.move_up_button.set_image(Gtk.Image(icon_name='go-up-symbolic')) 196 | self.move_up_button.connect('clicked', self.on_move_up_button_clicked) 197 | default_buttons_box.add(self.move_up_button) 198 | # Move down 199 | self.move_down_button = Gtk.Button() 200 | self.move_down_button.set_tooltip_text('Move down') 201 | self.move_down_button.set_image(Gtk.Image(icon_name='go-down-symbolic')) 202 | self.move_down_button.connect('clicked', self.on_move_down_button_clicked) 203 | default_buttons_box.add(self.move_down_button) 204 | # Delete 205 | self.delete_button = Gtk.Button() 206 | self.delete_button.set_tooltip_text('Delete') 207 | self.delete_button.set_image(Gtk.Image(icon_name='edit-delete-symbolic')) 208 | self.delete_button.connect('clicked', self.on_delete_button_clicked) 209 | default_buttons_box.add(self.delete_button) 210 | # Clear all 211 | self.clear_all_button = Gtk.Button() 212 | self.clear_all_button.set_tooltip_text('Clear all') 213 | self.clear_all_button.set_image(Gtk.Image(icon_name='edit-clear-all-symbolic')) 214 | self.clear_all_button.connect('clicked', self.on_clear_all_button_clicked) 215 | default_buttons_box.add(self.clear_all_button) 216 | # Initialise default buttons status 217 | self.reset_buttons() 218 | # Buttons box 219 | self.buttons_box = ButtonBox(linked=True) 220 | actionbar.pack_end(self.buttons_box) 221 | 222 | def on_add(self, callback): 223 | self.add_callback = callback 224 | 225 | def on_delete(self, callback): 226 | self.delete_callback = callback 227 | 228 | def on_activate(self, callback): 229 | self.activate_callback = callback 230 | 231 | def on_row_activated(self, listbox, row): 232 | if self.allow_moving: 233 | rows_count = len(self.get_rows()) 234 | index = row.get_index() 235 | # Move up 236 | enable_move_up = True if index > 0 else False 237 | self.move_up_button.set_sensitive(enable_move_up) 238 | # Move down 239 | enable_move_down = True if index < rows_count - 1 else False 240 | self.move_down_button.set_sensitive(enable_move_down) 241 | # Delete 242 | self.delete_button.set_sensitive(True) 243 | # Clear all 244 | self.clear_all_button.set_sensitive(True) 245 | # Invoke activate callback 246 | if self.activate_callback is not None: 247 | self.activate_callback() 248 | 249 | def on_size_allocate(self, listbox, event): 250 | if self.perform_scroll: 251 | adj = listbox.get_adjustment() 252 | adj.set_value(adj.get_upper() - adj.get_page_size()) 253 | self.perform_scroll = False 254 | 255 | def add_button(self, button): 256 | self.buttons_box.add(button) 257 | 258 | def append_text(self, text): 259 | # add new row 260 | row = Gtk.ListBoxRow() 261 | label = Gtk.Label(text, xalign=0, margin=5) 262 | row.add(label) 263 | self.listbox.add(row) 264 | self.listbox.show_all() 265 | self.perform_scroll = True 266 | self.select_row(row) 267 | if self.add_callback is not None: 268 | self.add_callback() 269 | 270 | def select_row(self, row): 271 | self.listbox.select_row(row) 272 | self.on_row_activated(self.listbox, row) 273 | 274 | def get_rows(self): 275 | return self.listbox.get_children() 276 | 277 | def is_empty(self): 278 | return len(self.get_rows()) == 0 279 | 280 | def get_row_text(self, row): 281 | label = row.get_children()[0] 282 | return label.get_text() 283 | 284 | def reset_buttons(self): 285 | if self.allow_moving: 286 | self.move_up_button.set_sensitive(False) 287 | self.move_down_button.set_sensitive(False) 288 | self.delete_button.set_sensitive(False) 289 | if self.is_empty(): 290 | self.clear_all_button.set_sensitive(False) 291 | 292 | def remove_row(self, row, reset=True): 293 | row_index = row.get_index() 294 | self.listbox.remove(row) 295 | if reset: 296 | self.reset_buttons() 297 | if self.delete_callback is not None: 298 | self.delete_callback(row_index) 299 | 300 | def on_delete_button_clicked(self, button): 301 | row = self.listbox.get_selected_row() 302 | self.remove_row(row) 303 | 304 | def move_row(self, row, new_index): 305 | self.listbox.select_row(None) # remove selection 306 | self.listbox.remove(row) 307 | self.listbox.insert(row, new_index) 308 | self.select_row(row) 309 | 310 | def on_move_up_button_clicked(self, button): 311 | row = self.listbox.get_selected_row() 312 | if row: 313 | index = row.get_index() 314 | self.move_row(row, index - 1) 315 | 316 | def on_move_down_button_clicked(self, button): 317 | row = self.listbox.get_selected_row() 318 | if row: 319 | index = row.get_index() 320 | self.move_row(row, index + 1) 321 | 322 | def clear(self): 323 | for row in self.get_rows(): 324 | self.remove_row(row, False) 325 | self.reset_buttons() 326 | 327 | def on_clear_all_button_clicked(self, button): 328 | dialog = Gtk.MessageDialog(text='Confirm clear all?', transient_for=self.parent, buttons=Gtk.ButtonsType.OK_CANCEL, message_type=Gtk.MessageType.QUESTION) 329 | response = dialog.run() 330 | dialog.destroy() 331 | 332 | # We only clear when the user presses the OK button 333 | if response == Gtk.ResponseType.OK: 334 | self.clear() 335 | 336 | class SpinButton(Gtk.SpinButton): 337 | 338 | def __init__(self, min=0, max=100, value=0, step=1, page_step=5): 339 | adjustment = Gtk.Adjustment(value=value, lower=min, upper=max, step_increment=step, page_increment=page_step, page_size=0) 340 | Gtk.SpinButton.__init__(self, adjustment=adjustment) 341 | 342 | class ImageLabel(Gtk.Box): 343 | 344 | def __init__(self, image, text, padding=0): 345 | Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=3) 346 | self.set_border_width(padding) 347 | self.add(image) 348 | self.label = Gtk.Label(text, ellipsize=Pango.EllipsizeMode.END) 349 | self.label.set_tooltip_text(text) 350 | self.add(self.label) 351 | 352 | def get_text(self): 353 | return self.label.get_text() 354 | 355 | class StackListBox(Gtk.Box): 356 | 357 | def __init__(self): 358 | Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=5) 359 | self.count = 0 360 | frame = Gtk.Frame() 361 | scrolled_window = Gtk.ScrolledWindow(hscrollbar_policy=Gtk.PolicyType.NEVER) 362 | self.listbox = Gtk.ListBox() 363 | self.listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) 364 | self.listbox.connect('row-activated', self.on_row_activated) 365 | scrolled_window.add(self.listbox) 366 | frame.add(scrolled_window) 367 | self.pack_start(frame, True, True, 0) 368 | self.stack = Gtk.Stack() 369 | self.pack_end(self.stack, False, False, 0) 370 | 371 | def on_row_activated(self, listbox, row): 372 | name = row.get_children()[0].get_text() 373 | self.stack.set_visible_child_name(name) 374 | 375 | def append(self, label, widget): 376 | # add listbox label 377 | self.listbox.add(label) 378 | if self.count == 0: # select first row 379 | self.listbox.select_row(self.listbox.get_row_at_index(self.count)) 380 | # add stack widget 381 | self.stack.add_named(widget, label.get_text()) 382 | self.count += 1 383 | 384 | class ButtonBox(Gtk.Box): 385 | 386 | def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=5, centered=False, linked=False): 387 | Gtk.Box.__init__(self, orientation=orientation) 388 | self.buttons_container = Gtk.Box(orientation=orientation) 389 | self.orientation = orientation 390 | self.linked = linked 391 | # set centered 392 | if centered: 393 | self.pack_start(self.buttons_container, True, False, 0) 394 | else: 395 | self.pack_start(self.buttons_container, False, False, 0) 396 | # set linked 397 | if linked: 398 | Gtk.StyleContext.add_class(self.buttons_container.get_style_context(), Gtk.STYLE_CLASS_LINKED) 399 | else: 400 | self.buttons_container.set_spacing(spacing) 401 | 402 | def add(self, button): 403 | if self.orientation == Gtk.Orientation.VERTICAL and not self.linked: 404 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 405 | hbox.pack_start(button, True, False, 0) 406 | self.buttons_container.add(hbox) 407 | else: 408 | self.buttons_container.add(button) 409 | 410 | class MessageBox(Gtk.Box): 411 | 412 | def __init__(self, text=None, color='black', enable_buttons=False): 413 | Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=5) 414 | self.enable_buttons = enable_buttons 415 | # label 416 | self.label = Gtk.Label(text) 417 | self.label.modify_fg(Gtk.StateType.NORMAL, Gdk.color_parse(color)) 418 | self.add(self.label) 419 | # question buttons 420 | if enable_buttons: 421 | self.button_box = ButtonBox(linked=True) 422 | self.pack_end(self.button_box, False, False, 0) 423 | # yes 424 | self.yes_button = Gtk.Button() 425 | self.yes_button.set_tooltip_text('Yes') 426 | self.yes_button.set_image(Gtk.Image(icon_name='emblem-ok-symbolic')) 427 | self.button_box.add(self.yes_button) 428 | # no 429 | self.no_button = Gtk.Button() 430 | self.no_button.set_tooltip_text('No') 431 | self.no_button.set_image(Gtk.Image(icon_name='window-close-symbolic')) 432 | self.button_box.add(self.no_button) 433 | 434 | def print_message(self, text, is_question=False): 435 | self.label.set_text(text) 436 | if self.enable_buttons: 437 | if is_question: 438 | self.button_box.show() 439 | else: 440 | self.button_box.hide() 441 | self.show() 442 | 443 | class MenuButton(Gtk.Button): 444 | 445 | def __init__(self, text=None, position=Gtk.PositionType.BOTTOM, icon_name=None, padding=2): 446 | Gtk.Button.__init__(self, text) 447 | if icon_name is not None: 448 | self.set_image(Gtk.Image(icon_name=icon_name)) 449 | self.connect('clicked', self.on_clicked) 450 | # popover 451 | self.popover = Gtk.Popover(relative_to=self, position=position) 452 | self.popover.set_border_width(padding) 453 | 454 | def on_clicked(self, button): 455 | self.popover.show_all() 456 | 457 | def add(self, widget): 458 | self.popover.add(widget) 459 | 460 | class MenuImage(Gtk.EventBox): 461 | 462 | def __init__(self, icon_name='pan-down-symbolic', pixel_size=13, position=Gtk.PositionType.BOTTOM, padding=2): 463 | Gtk.EventBox.__init__(self) 464 | self.add(Gtk.Image(icon_name=icon_name, pixel_size=pixel_size)) 465 | self.connect('button-press-event', self.on_button_press) 466 | self.connect('enter-notify-event', self.on_enter_notify) 467 | # popover 468 | self.popover = Gtk.Popover(relative_to=self, position=position) 469 | self.popover.set_border_width(padding) 470 | 471 | def on_button_press(self, widget, event): 472 | self.popover.show_all() 473 | 474 | def on_enter_notify(self, widget, event): 475 | window = self.get_window() 476 | window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND1)) 477 | 478 | def set_widget(self, widget): 479 | self.popover.add(widget) 480 | 481 | class FileChooserButton(Gtk.FileChooserButton): 482 | 483 | def __init__(self, title, filter=None): 484 | Gtk.FileChooserButton.__init__(self, title=title) 485 | if filter is not None and len(filter) > 1: 486 | name, pattern = filter 487 | file_filter = Gtk.FileFilter() 488 | file_filter.set_name('%s (%s)' % (name, pattern)) 489 | file_filter.add_pattern(pattern) 490 | self.add_filter(file_filter) 491 | 492 | class MiniMap(Gtk.Frame): 493 | 494 | point_colors = { 495 | 'Monster': 'red', 496 | 'Resource': 'green', 497 | 'NPC': 'blue', 498 | 'None': 'black' 499 | } 500 | 501 | def __init__(self, background_color='#CECECE', show_grid=True, grid_color='#DDDDDD', grid_size=(15, 15), point_radius=3): 502 | Gtk.Frame.__init__(self) 503 | self.points = [] 504 | self.point_opacity = 0.7 505 | self.point_radius = point_radius 506 | self.show_grid = show_grid 507 | self.grid_color = grid_color 508 | self.grid_size = grid_size 509 | self.background_color = background_color 510 | self.use_origin_colors = False 511 | self.add_borders = False 512 | self.drawing_area = Gtk.DrawingArea() 513 | self.drawing_area.set_has_tooltip(True) 514 | self.drawing_area.connect('draw', self.on_draw) 515 | self.drawing_area.connect('query-tooltip', self.on_query_tooltip) 516 | self.add(self.drawing_area) 517 | 518 | def set_use_origin_colors(self, value): 519 | self.use_origin_colors = value 520 | if self.points: 521 | self.drawing_area.queue_draw() 522 | 523 | def set_add_borders(self, value): 524 | self.add_borders = value 525 | if self.points: 526 | self.drawing_area.queue_draw() 527 | 528 | def get_color_key(self): 529 | return 'origin_color' if self.use_origin_colors else 'color' 530 | 531 | def add_point(self, point, name=None, color=None, redraw=True): 532 | # set point coordinates 533 | new_point = { 534 | 'x': point['x'], 535 | 'y': point['y'], 536 | 'width': point['width'], 537 | 'height': point['height'] 538 | } 539 | # set point name 540 | if name is not None: 541 | new_point['name'] = name 542 | elif 'name' in point: 543 | new_point['name'] = point['name'] 544 | else: 545 | new_point['name'] = None 546 | # set point color 547 | new_point['color'] = color 548 | new_point['origin_color'] = parse_color(point['color'], as_hex=True) if 'color' in point else None 549 | # add point 550 | self.points.append(new_point) 551 | if redraw: 552 | self.drawing_area.queue_draw() 553 | 554 | def add_points(self, points, name=None, color=None): 555 | for point in points: 556 | self.add_point(point, name, color, False) 557 | self.drawing_area.queue_draw() 558 | 559 | def remove_point(self, index): 560 | if 0 <= index < len(self.points): 561 | del self.points[index] 562 | self.drawing_area.queue_draw() 563 | 564 | def clear(self): 565 | if self.points: 566 | self.points = [] 567 | self.drawing_area.queue_draw() 568 | 569 | def on_draw(self, widget, cr): 570 | drawing_area = widget.get_allocation() 571 | square_width, square_height = self.grid_size 572 | cr.set_line_width(1) 573 | # set color function 574 | def set_color(value, opacity=1.0): 575 | color = Gdk.color_parse(value) 576 | cr.set_source_rgba(float(color.red) / 65535, float(color.green) / 65535, float(color.blue) / 65535, opacity) 577 | # fill background with color 578 | if self.background_color: 579 | cr.rectangle(0, 0, drawing_area.width, drawing_area.height) 580 | set_color(self.background_color) 581 | cr.fill() 582 | # draw grid lines 583 | if self.show_grid: 584 | set_color(self.grid_color) 585 | # draw vertical lines 586 | for x in range(square_width, drawing_area.width, square_width + 1): # +1 for line width 587 | cr.move_to(x + 0.5, 0) # +0.5 for smooth line 588 | cr.line_to(x + 0.5, drawing_area.height) 589 | # draw horizontal lines 590 | for y in range(square_height, drawing_area.height, square_height + 1): 591 | cr.move_to(0, y + 0.5) 592 | cr.line_to(drawing_area.width, y + 0.5) 593 | cr.stroke() 594 | # draw points 595 | for point in self.points: 596 | # fit point to drawing area (should keep here, because it's useful when drawing area get resized) 597 | x, y = fit_position_to_destination(point['x'], point['y'], point['width'], point['height'], drawing_area.width, drawing_area.height) 598 | if self.add_borders: 599 | set_color('black') 600 | cr.arc(x, y, self.point_radius, 0, 2*math.pi) 601 | if self.add_borders: 602 | cr.stroke_preserve() 603 | color_key = self.get_color_key() 604 | color = self.point_colors['None'] if point[color_key] is None else point[color_key] 605 | set_color(color, self.point_opacity) 606 | cr.fill() 607 | 608 | def get_tooltip_widget(self, point): 609 | # on draw function 610 | def on_draw(widget, cr): 611 | cr.set_line_width(1) 612 | # draw point 613 | color_key = self.get_color_key() 614 | color = Gdk.color_parse(point[color_key]) 615 | cr.set_source_rgba(float(color.red) / 65535, float(color.green) / 65535, float(color.blue) / 65535, self.point_opacity) 616 | cr.arc(self.point_radius, self.point_radius, self.point_radius, 0, 2*math.pi) 617 | cr.fill() 618 | # tooltip widget 619 | if point['name'] is not None: 620 | widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=3) 621 | color_key = self.get_color_key() 622 | if point[color_key] is not None: 623 | drawing_area = Gtk.DrawingArea() 624 | point_diameter = self.point_radius*2 625 | drawing_area.set_size_request(point_diameter, point_diameter) 626 | drawing_area.connect('draw', on_draw) 627 | box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 628 | box.pack_start(drawing_area, True, False, 0) 629 | widget.add(box) 630 | widget.add(Gtk.Label(point['name'])) 631 | widget.show_all() 632 | else: 633 | widget = None 634 | return widget 635 | 636 | def on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip): 637 | drawing_area = self.drawing_area.get_allocation() 638 | tooltip_widget = None 639 | # check if a point is hovered 640 | for point in self.points: 641 | # fit point to drawing area 642 | point_x, point_y = fit_position_to_destination(point['x'], point['y'], point['width'], point['height'], drawing_area.width, drawing_area.height) 643 | # TODO: the check below should be circular, not rectangular 644 | if point_x - self.point_radius <= x <= point_x + self.point_radius and point_y - self.point_radius <= y <= point_y + self.point_radius: 645 | tooltip_widget = self.get_tooltip_widget(point) 646 | break 647 | # if so 648 | if tooltip_widget is not None: 649 | # set tooltip widget 650 | tooltip.set_custom(tooltip_widget) 651 | # show the tooltip 652 | return True 653 | else: 654 | return False 655 | -------------------------------------------------------------------------------- /pyscreeze/__init__.py: -------------------------------------------------------------------------------- 1 | # PyScreeze 2 | 3 | """ 4 | NOTE: 5 | Apparently Pillow support on Ubuntu 64-bit has several additional steps since it doesn't have JPEG/PNG support out of the box. Description here: 6 | 7 | https://stackoverflow.com/questions/7648200/pip-install-pil-e-tickets-1-no-jpeg-png-support 8 | http://ubuntuforums.org/showthread.php?t=1751455 9 | """ 10 | 11 | __version__ = '0.1.26' 12 | 13 | import collections 14 | import datetime 15 | import functools 16 | import os 17 | import subprocess 18 | import sys 19 | import time 20 | import errno 21 | 22 | from contextlib import contextmanager 23 | 24 | try: 25 | from PIL import Image 26 | from PIL import ImageOps 27 | from PIL import ImageDraw 28 | if sys.platform == 'win32': # TODO - Pillow now supports ImageGrab on macOS. 29 | from PIL import ImageGrab 30 | _PILLOW_UNAVAILABLE = False 31 | except ImportError: 32 | # We ignore this because failures due to Pillow not being installed 33 | # should only happen when the functions that specifically depend on 34 | # Pillow are called. The main use case is when PyAutoGUI imports 35 | # PyScreeze, but Pillow isn't installed because the user is running 36 | # some platform/version of Python that Pillow doesn't support, then 37 | # importing PyAutoGUI should not automatically fail because it 38 | # imports PyScreeze. 39 | # So we have a `pass` statement here since a failure to import 40 | # Pillow shouldn't crash PyScreeze. 41 | _PILLOW_UNAVAILABLE = True 42 | 43 | 44 | try: 45 | import cv2, numpy 46 | useOpenCV = True 47 | RUNNING_CV_2 = cv2.__version__[0] < '3' 48 | except ImportError: 49 | useOpenCV = False 50 | 51 | RUNNING_PYTHON_2 = sys.version_info[0] == 2 52 | if useOpenCV: 53 | if RUNNING_CV_2: 54 | LOAD_COLOR = cv2.CV_LOAD_IMAGE_COLOR 55 | LOAD_GRAYSCALE = cv2.CV_LOAD_IMAGE_GRAYSCALE 56 | else: 57 | LOAD_COLOR = cv2.IMREAD_COLOR 58 | LOAD_GRAYSCALE = cv2.IMREAD_GRAYSCALE 59 | 60 | if not RUNNING_PYTHON_2: 61 | unicode = str # On Python 3, all the isinstance(spam, (str, unicode)) calls will work the same as Python 2. 62 | 63 | if sys.platform == 'win32': 64 | # On Windows, the monitor scaling can be set to something besides normal 100%. 65 | # PyScreeze and Pillow needs to account for this to make accurate screenshots. 66 | # TODO - How does macOS and Linux handle monitor scaling? 67 | import ctypes 68 | try: 69 | ctypes.windll.user32.SetProcessDPIAware() 70 | except AttributeError: 71 | pass # Windows XP doesn't support monitor scaling, so just do nothing. 72 | 73 | 74 | GRAYSCALE_DEFAULT = False 75 | 76 | # For version 0.1.19 I changed it so that ImageNotFoundException was raised 77 | # instead of returning None. In hindsight, this change came too late, so I'm 78 | # changing it back to returning None. But I'm also including this option for 79 | # folks who would rather have it raise an exception. 80 | USE_IMAGE_NOT_FOUND_EXCEPTION = False 81 | 82 | scrotExists = False 83 | try: 84 | if sys.platform not in ('java', 'darwin', 'win32'): 85 | whichProc = subprocess.Popen( 86 | ['which', 'scrot'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 87 | scrotExists = whichProc.wait() == 0 88 | except OSError as ex: 89 | if ex.errno == errno.ENOENT: 90 | # if there is no "which" program to find scrot, then assume there 91 | # is no scrot. 92 | pass 93 | else: 94 | raise 95 | 96 | 97 | if sys.platform == 'win32': 98 | from ctypes import windll 99 | 100 | # win32 DC(DeviceContext) Manager 101 | @contextmanager 102 | def __win32_openDC(hWnd): 103 | """ 104 | TODO 105 | """ 106 | hDC = windll.user32.GetDC(hWnd) 107 | if hDC == 0: #NULL 108 | raise WindowsError("windll.user32.GetDC failed : return NULL") 109 | try: 110 | yield hDC 111 | finally: 112 | if windll.user32.ReleaseDC(hWnd, hDC) == 0: 113 | raise WindowsError("windll.user32.ReleaseDC failed : return 0") 114 | 115 | Box = collections.namedtuple('Box', 'left top width height') 116 | Point = collections.namedtuple('Point', 'x y') 117 | RGB = collections.namedtuple('RGB', 'red green blue') 118 | 119 | class PyScreezeException(Exception): 120 | pass # This is a generic exception class raised when a PyScreeze-related error happens. 121 | 122 | class ImageNotFoundException(PyScreezeException): 123 | pass # This is an exception class raised when the locate functions fail to locate an image. 124 | 125 | 126 | def requiresPillow(wrappedFunction): 127 | """ 128 | A decorator that marks a function as requiring Pillow to be installed. 129 | This raises PyScreezeException if Pillow wasn't imported. 130 | """ 131 | @functools.wraps(wrappedFunction) 132 | def wrapper(*args, **kwargs): 133 | if _PILLOW_UNAVAILABLE: 134 | raise PyScreezeException('The Pillow package is required to use this function.') 135 | return wrappedFunction(*args, **kwargs) 136 | return wrapper 137 | 138 | def _load_cv2(img, grayscale=None): 139 | """ 140 | TODO 141 | """ 142 | # load images if given filename, or convert as needed to opencv 143 | # Alpha layer just causes failures at this point, so flatten to RGB. 144 | # RGBA: load with -1 * cv2.CV_LOAD_IMAGE_COLOR to preserve alpha 145 | # to matchTemplate, need template and image to be the same wrt having alpha 146 | 147 | if grayscale is None: 148 | grayscale = GRAYSCALE_DEFAULT 149 | if isinstance(img, (str, unicode)): 150 | # The function imread loads an image from the specified file and 151 | # returns it. If the image cannot be read (because of missing 152 | # file, improper permissions, unsupported or invalid format), 153 | # the function returns an empty matrix 154 | # http://docs.opencv.org/3.0-beta/modules/imgcodecs/doc/reading_and_writing_images.html 155 | if grayscale: 156 | img_cv = cv2.imread(img, LOAD_GRAYSCALE) 157 | else: 158 | img_cv = cv2.imread(img, LOAD_COLOR) 159 | if img_cv is None: 160 | raise IOError("Failed to read %s because file is missing, " 161 | "has improper permissions, or is an " 162 | "unsupported or invalid format" % img) 163 | elif isinstance(img, numpy.ndarray): 164 | # don't try to convert an already-gray image to gray 165 | if grayscale and len(img.shape) == 3: # and img.shape[2] == 3: 166 | img_cv = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 167 | else: 168 | img_cv = img 169 | elif hasattr(img, 'convert'): 170 | # assume its a PIL.Image, convert to cv format 171 | img_array = numpy.array(img.convert('RGB')) 172 | img_cv = img_array[:, :, ::-1].copy() # -1 does RGB -> BGR 173 | if grayscale: 174 | img_cv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY) 175 | else: 176 | raise TypeError('expected an image filename, OpenCV numpy array, or PIL image') 177 | return img_cv 178 | 179 | 180 | def _locateAll_opencv(needleImage, haystackImage, grayscale=None, limit=10000, region=None, step=1, 181 | confidence=0.999): 182 | """ 183 | TODO - rewrite this 184 | faster but more memory-intensive than pure python 185 | step 2 skips every other row and column = ~3x faster but prone to miss; 186 | to compensate, the algorithm automatically reduces the confidence 187 | threshold by 5% (which helps but will not avoid all misses). 188 | limitations: 189 | - OpenCV 3.x & python 3.x not tested 190 | - RGBA images are treated as RBG (ignores alpha channel) 191 | """ 192 | if grayscale is None: 193 | grayscale = GRAYSCALE_DEFAULT 194 | 195 | confidence = float(confidence) 196 | 197 | needleImage = _load_cv2(needleImage, grayscale) 198 | needleHeight, needleWidth = needleImage.shape[:2] 199 | haystackImage = _load_cv2(haystackImage, grayscale) 200 | 201 | if region: 202 | haystackImage = haystackImage[region[1]:region[1]+region[3], 203 | region[0]:region[0]+region[2]] 204 | else: 205 | region = (0, 0) # full image; these values used in the yield statement 206 | if (haystackImage.shape[0] < needleImage.shape[0] or 207 | haystackImage.shape[1] < needleImage.shape[1]): 208 | # avoid semi-cryptic OpenCV error below if bad size 209 | raise ValueError('needle dimension(s) exceed the haystack image or region dimensions') 210 | 211 | if step == 2: 212 | confidence *= 0.95 213 | needleImage = needleImage[::step, ::step] 214 | haystackImage = haystackImage[::step, ::step] 215 | else: 216 | step = 1 217 | 218 | # get all matches at once, credit: https://stackoverflow.com/questions/7670112/finding-a-subimage-inside-a-numpy-image/9253805#9253805 219 | result = cv2.matchTemplate(haystackImage, needleImage, cv2.TM_CCOEFF_NORMED) 220 | match_indices = numpy.arange(result.size)[(result > confidence).flatten()] 221 | matches = numpy.unravel_index(match_indices[:limit], result.shape) 222 | 223 | if len(matches[0]) == 0: 224 | if USE_IMAGE_NOT_FOUND_EXCEPTION: 225 | raise ImageNotFoundException('Could not locate the image (highest confidence = %.3f)' % result.max()) 226 | else: 227 | return 228 | 229 | # use a generator for API consistency: 230 | matchx = matches[1] * step + region[0] # vectorized 231 | matchy = matches[0] * step + region[1] 232 | for x, y in zip(matchx, matchy): 233 | yield Box(x, y, needleWidth, needleHeight) 234 | 235 | 236 | # TODO - We should consider renaming _locateAll_python to _locateAll_pillow, since Pillow is the real dependency. 237 | @requiresPillow 238 | def _locateAll_python(needleImage, haystackImage, grayscale=None, limit=None, region=None, step=1): 239 | """ 240 | TODO 241 | """ 242 | # setup all the arguments 243 | if grayscale is None: 244 | grayscale = GRAYSCALE_DEFAULT 245 | 246 | needleFileObj = None 247 | if isinstance(needleImage, (str, unicode)): 248 | # 'image' is a filename, load the Image object 249 | needleFileObj = open(needleImage, 'rb') 250 | needleImage = Image.open(needleFileObj) 251 | 252 | haystackFileObj = None 253 | if isinstance(haystackImage, (str, unicode)): 254 | # 'image' is a filename, load the Image object 255 | haystackFileObj = open(haystackImage, 'rb') 256 | haystackImage = Image.open(haystackFileObj) 257 | 258 | if region is not None: 259 | haystackImage = haystackImage.crop((region[0], region[1], region[0] + region[2], region[1] + region[3])) 260 | else: 261 | region = (0, 0) # set to 0 because the code always accounts for a region 262 | 263 | if grayscale: # if grayscale mode is on, convert the needle and haystack images to grayscale 264 | needleImage = ImageOps.grayscale(needleImage) 265 | haystackImage = ImageOps.grayscale(haystackImage) 266 | else: 267 | # if not using grayscale, make sure we are comparing RGB images, not RGBA images. 268 | if needleImage.mode == 'RGBA': 269 | needleImage = needleImage.convert('RGB') 270 | if haystackImage.mode == 'RGBA': 271 | haystackImage = haystackImage.convert('RGB') 272 | 273 | # setup some constants we'll be using in this function 274 | needleWidth, needleHeight = needleImage.size 275 | haystackWidth, haystackHeight = haystackImage.size 276 | 277 | needleImageData = tuple(needleImage.getdata()) 278 | haystackImageData = tuple(haystackImage.getdata()) 279 | 280 | needleImageRows = [needleImageData[y * needleWidth:(y+1) * needleWidth] for y in range(needleHeight)] # LEFT OFF - check this 281 | needleImageFirstRow = needleImageRows[0] 282 | 283 | assert len(needleImageFirstRow) == needleWidth, 'For some reason, the calculated width of first row of the needle image is not the same as the width of the image.' 284 | assert [len(row) for row in needleImageRows] == [needleWidth] * needleHeight, 'For some reason, the needleImageRows aren\'t the same size as the original image.' 285 | 286 | numMatchesFound = 0 287 | 288 | # NOTE: After running tests/benchmarks.py on the following code, it seem that having a step 289 | # value greater than 1 does not give *any* significant performance improvements. 290 | # Since using a step higher than 1 makes for less accurate matches, it will be 291 | # set to 1. 292 | step = 1 # hard-code step as 1 until a way to improve it can be figured out. 293 | 294 | if step == 1: 295 | firstFindFunc = _kmp 296 | else: 297 | firstFindFunc = _steppingFind 298 | 299 | 300 | for y in range(haystackHeight): # start at the leftmost column 301 | for matchx in firstFindFunc(needleImageFirstRow, haystackImageData[y * haystackWidth:(y+1) * haystackWidth], step): 302 | foundMatch = True 303 | for searchy in range(1, needleHeight, step): 304 | haystackStart = (searchy + y) * haystackWidth + matchx 305 | if needleImageData[searchy * needleWidth:(searchy+1) * needleWidth] != haystackImageData[haystackStart:haystackStart + needleWidth]: 306 | foundMatch = False 307 | break 308 | if foundMatch: 309 | # Match found, report the x, y, width, height of where the matching region is in haystack. 310 | numMatchesFound += 1 311 | yield Box(matchx + region[0], y + region[1], needleWidth, needleHeight) 312 | if limit is not None and numMatchesFound >= limit: 313 | # Limit has been reached. Close file handles. 314 | if needleFileObj is not None: 315 | needleFileObj.close() 316 | if haystackFileObj is not None: 317 | haystackFileObj.close() 318 | return 319 | 320 | 321 | # There was no limit or the limit wasn't reached, but close the file handles anyway. 322 | if needleFileObj is not None: 323 | needleFileObj.close() 324 | if haystackFileObj is not None: 325 | haystackFileObj.close() 326 | 327 | if numMatchesFound == 0: 328 | if USE_IMAGE_NOT_FOUND_EXCEPTION: 329 | raise ImageNotFoundException('Could not locate the image.') 330 | else: 331 | return 332 | 333 | 334 | def locate(needleImage, haystackImage, **kwargs): 335 | """ 336 | TODO 337 | """ 338 | # Note: The gymnastics in this function is because we want to make sure to exhaust the iterator so that the needle and haystack files are closed in locateAll. 339 | kwargs['limit'] = 1 340 | points = tuple(locateAll(needleImage, haystackImage, **kwargs)) 341 | if len(points) > 0: 342 | return points[0] 343 | else: 344 | if USE_IMAGE_NOT_FOUND_EXCEPTION: 345 | raise ImageNotFoundException('Could not locate the image.') 346 | else: 347 | return None 348 | 349 | 350 | def locateOnScreen(image, minSearchTime=0, **kwargs): 351 | """TODO - rewrite this 352 | minSearchTime - amount of time in seconds to repeat taking 353 | screenshots and trying to locate a match. The default of 0 performs 354 | a single search. 355 | """ 356 | start = time.time() 357 | while True: 358 | try: 359 | screenshotIm = screenshot(region=None) # the locateAll() function must handle cropping to return accurate coordinates, so don't pass a region here. 360 | retVal = locate(image, screenshotIm, **kwargs) 361 | try: 362 | screenshotIm.fp.close() 363 | except AttributeError: 364 | # Screenshots on Windows won't have an fp since they came from 365 | # ImageGrab, not a file. Screenshots on Linux will have fp set 366 | # to None since the file has been unlinked 367 | pass 368 | if retVal or time.time() - start > minSearchTime: 369 | return retVal 370 | except ImageNotFoundException: 371 | if time.time() - start > minSearchTime: 372 | if USE_IMAGE_NOT_FOUND_EXCEPTION: 373 | raise 374 | else: 375 | return None 376 | 377 | 378 | def locateAllOnScreen(image, **kwargs): 379 | """ 380 | TODO 381 | """ 382 | 383 | # TODO - Should this raise an exception if zero instances of the image can be found on the screen, instead of always returning a generator? 384 | screenshotIm = screenshot(region=None) # the locateAll() function must handle cropping to return accurate coordinates, so don't pass a region here. 385 | retVal = locateAll(image, screenshotIm, **kwargs) 386 | try: 387 | screenshotIm.fp.close() 388 | except AttributeError: 389 | # Screenshots on Windows won't have an fp since they came from 390 | # ImageGrab, not a file. Screenshots on Linux will have fp set 391 | # to None since the file has been unlinked 392 | pass 393 | return retVal 394 | 395 | 396 | def locateCenterOnScreen(image, **kwargs): 397 | """ 398 | TODO 399 | """ 400 | coords = locateOnScreen(image, **kwargs) 401 | if coords is None: 402 | return None 403 | else: 404 | return center(coords) 405 | 406 | 407 | @requiresPillow 408 | def showRegionOnScreen(region, outlineColor='red', filename='_showRegionOnScreen.png'): 409 | """ 410 | TODO 411 | """ 412 | # TODO - This function is useful! Document it! 413 | screenshotIm = screenshot() 414 | draw = ImageDraw.Draw(screenshotIm) 415 | region = (region[0], region[1], region[2] + region[0], region[3] + region[1]) # convert from (left, top, right, bottom) to (left, top, width, height) 416 | draw.rectangle(region, outline=outlineColor) 417 | screenshotIm.save(filename) 418 | 419 | 420 | @requiresPillow 421 | def _screenshot_win32(imageFilename=None, region=None): 422 | """ 423 | TODO 424 | """ 425 | # TODO - Use the winapi to get a screenshot, and compare performance with ImageGrab.grab() 426 | # https://stackoverflow.com/a/3586280/1893164 427 | im = ImageGrab.grab() 428 | if region is not None: 429 | assert len(region) == 4, 'region argument must be a tuple of four ints' 430 | region = [int(x) for x in region] 431 | im = im.crop((region[0], region[1], region[2] + region[0], region[3] + region[1])) 432 | if imageFilename is not None: 433 | im.save(imageFilename) 434 | return im 435 | 436 | 437 | def _screenshot_osx(imageFilename=None, region=None): 438 | """ 439 | TODO 440 | """ 441 | # TODO - use tmp name for this file. 442 | if imageFilename is None: 443 | tmpFilename = 'screenshot%s.png' % (datetime.datetime.now().strftime('%Y-%m%d_%H-%M-%S-%f')) 444 | else: 445 | tmpFilename = imageFilename 446 | subprocess.call(['screencapture', '-x', tmpFilename]) 447 | im = Image.open(tmpFilename) 448 | 449 | if region is not None: 450 | assert len(region) == 4, 'region argument must be a tuple of four ints' 451 | region = [int(x) for x in region] 452 | im = im.crop((region[0], region[1], region[2] + region[0], region[3] + region[1])) 453 | os.unlink(tmpFilename) # delete image of entire screen to save cropped version 454 | im.save(tmpFilename) 455 | else: 456 | # force loading before unlinking, Image.open() is lazy 457 | im.load() 458 | 459 | if imageFilename is None: 460 | os.unlink(tmpFilename) 461 | return im 462 | 463 | 464 | def _screenshot_linux(imageFilename=None, region=None): 465 | """ 466 | TODO 467 | """ 468 | if not scrotExists: 469 | raise NotImplementedError('"scrot" must be installed to use screenshot functions in Linux. Run: sudo apt-get install scrot') 470 | if imageFilename is None: 471 | tmpFilename = '.screenshot%s.png' % (datetime.datetime.now().strftime('%Y-%m%d_%H-%M-%S-%f')) 472 | else: 473 | tmpFilename = imageFilename 474 | if scrotExists: 475 | subprocess.call(['scrot', '-z', tmpFilename]) 476 | im = Image.open(tmpFilename) 477 | 478 | if region is not None: 479 | assert len(region) == 4, 'region argument must be a tuple of four ints' 480 | region = [int(x) for x in region] 481 | im = im.crop((region[0], region[1], region[2] + region[0], region[3] + region[1])) 482 | os.unlink(tmpFilename) # delete image of entire screen to save cropped version 483 | im.save(tmpFilename) 484 | else: 485 | # force loading before unlinking, Image.open() is lazy 486 | im.load() 487 | 488 | if imageFilename is None: 489 | os.unlink(tmpFilename) 490 | return im 491 | else: 492 | raise Exception('The scrot program must be installed to take a screenshot with PyScreeze on Linux. Run: sudo apt-get install scrot') 493 | 494 | 495 | 496 | def _kmp(needle, haystack, _dummy): # Knuth-Morris-Pratt search algorithm implementation (to be used by screen capture) 497 | """ 498 | TODO 499 | """ 500 | # build table of shift amounts 501 | shifts = [1] * (len(needle) + 1) 502 | shift = 1 503 | for pos in range(len(needle)): 504 | while shift <= pos and needle[pos] != needle[pos-shift]: 505 | shift += shifts[pos-shift] 506 | shifts[pos+1] = shift 507 | 508 | # do the actual search 509 | startPos = 0 510 | matchLen = 0 511 | for c in haystack: 512 | while matchLen == len(needle) or \ 513 | matchLen >= 0 and needle[matchLen] != c: 514 | startPos += shifts[matchLen] 515 | matchLen -= shifts[matchLen] 516 | matchLen += 1 517 | if matchLen == len(needle): 518 | yield startPos 519 | 520 | 521 | def _steppingFind(needle, haystack, step): 522 | """ 523 | TODO 524 | """ 525 | for startPos in range(0, len(haystack) - len(needle) + 1): 526 | foundMatch = True 527 | for pos in range(0, len(needle), step): 528 | if haystack[startPos + pos] != needle[pos]: 529 | foundMatch = False 530 | break 531 | if foundMatch: 532 | yield startPos 533 | 534 | 535 | def center(coords): 536 | """ 537 | Returns a `Point` object with the x and y set to an integer determined by the format of `coords`. 538 | 539 | The `coords` argument is a 4-integer tuple of (left, top, width, height). 540 | 541 | For example: 542 | 543 | >>> center((10, 10, 6, 8)) 544 | Point(x=13, y=14) 545 | >>> center((10, 10, 7, 9)) 546 | Point(x=13, y=14) 547 | >>> center((10, 10, 8, 10)) 548 | Point(x=14, y=15) 549 | """ 550 | 551 | # TODO - one day, add code to handle a Box namedtuple. 552 | return Point(coords[0] + int(coords[2] / 2), coords[1] + int(coords[3] / 2)) 553 | 554 | 555 | def pixelMatchesColor(x, y, expectedRGBColor, tolerance=0): 556 | """ 557 | TODO 558 | """ 559 | pix = pixel(x, y) 560 | if len(pix) == 3 or len(expectedRGBColor) == 3: #RGB mode 561 | r, g, b = pix[:3] 562 | exR, exG, exB = expectedRGBColor[:3] 563 | return (abs(r - exR) <= tolerance) and (abs(g - exG) <= tolerance) and (abs(b - exB) <= tolerance) 564 | elif len(pix) == 4 and len(expectedRGBColor) == 4: #RGBA mode 565 | r, g, b, a = pix 566 | exR, exG, exB, exA = expectedRGBColor 567 | return (abs(r - exR) <= tolerance) and (abs(g - exG) <= tolerance) and (abs(b - exB) <= tolerance) and (abs(a - exA) <= tolerance) 568 | else: 569 | assert False, 'Color mode was expected to be length 3 (RGB) or 4 (RGBA), but pixel is length %s and expectedRGBColor is length %s' % (len(pix), len(expectedRGBColor)) 570 | 571 | def pixel(x, y): 572 | """ 573 | TODO 574 | """ 575 | if sys.platform == 'win32': 576 | # On Windows, calling GetDC() and GetPixel() is twice as fast as using our screenshot() function. 577 | with __win32_openDC(0) as hdc: # handle will be released automatically 578 | color = windll.gdi32.GetPixel(hdc, x, y) 579 | if color < 0: 580 | raise WindowsError("windll.gdi32.GetPixel failed : return {}".format(color)) 581 | # color is in the format 0xbbggrr https://msdn.microsoft.com/en-us/library/windows/desktop/dd183449(v=vs.85).aspx 582 | bbggrr = "{:0>6x}".format(color) # bbggrr => 'bbggrr' (hex) 583 | b, g, r = (int(bbggrr[i:i+2], 16) for i in range(0, 6, 2)) 584 | return (r, g, b) 585 | else: 586 | # Need to select only the first three values of the color in 587 | # case the returned pixel has an alpha channel 588 | return RGB(*(screenshot().getpixel((x, y))[:3])) 589 | 590 | 591 | # set the screenshot() function based on the platform running this module 592 | if sys.platform.startswith('java'): 593 | raise NotImplementedError('Jython is not yet supported by PyScreeze.') 594 | elif sys.platform == 'darwin': 595 | screenshot = _screenshot_osx 596 | elif sys.platform == 'win32': 597 | screenshot = _screenshot_win32 598 | else: # TODO - Make this more specific. "Anything else" does not necessarily mean "Linux". 599 | screenshot = _screenshot_linux 600 | 601 | grab = screenshot # for compatibility with Pillow/PIL's ImageGrab module. 602 | 603 | # set the locateAll function to use opencv if possible; python 3 needs opencv 3.0+ 604 | # TODO - Should this raise an exception if zero instances of the image can be found on the screen, instead of always returning a generator? 605 | if useOpenCV: 606 | locateAll = _locateAll_opencv 607 | if not RUNNING_PYTHON_2 and cv2.__version__ < '3': 608 | locateAll = _locateAll_python 609 | else: 610 | locateAll = _locateAll_python 611 | --------------------------------------------------------------------------------