├── 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 | #
Dindo Bot
2 |
3 | [](https://www.python.org/)
4 | [](https://www.gtk.org/)
5 | [](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 | [](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 |
--------------------------------------------------------------------------------