├── .gitignore ├── LICENSE ├── README.md ├── appctrl ├── __init__.py ├── battlenet.py └── wow.py ├── autoqueue.py ├── requirements.txt ├── ui ├── __init__.py ├── autoqueue.py └── autoqueue.ui └── utils └── screenshot.py /.gitignore: -------------------------------------------------------------------------------- 1 | .env/ 2 | __pycache__/ 3 | .vscode/ 4 | output/ 5 | *.au3 6 | *.sw* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 Charlee Li 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 《魔兽世界怀旧服》自动排队及防掉线工具 2 | =================================== 3 | 4 | 5 | 该工具仅限Win64下使用。开发环境为Windows 10,Python 3.6.2。 6 | 7 | ## 开发 8 | 9 | 执行下述命令构建开发环境。 10 | 11 | 12 | ``` 13 | D:\dev\autoqueue> python -m venv .env 14 | D:\dev\autoqueue> bin\Scripts\activate 15 | D:\dev\autoqueue> (.env) pip install -r requirements.txt 16 | ``` 17 | 18 | ### 结构 19 | 20 | 本工具采用PyQt5构建UI,采用pyautogui和pywin32作为窗体控制工具。 21 | 22 | ### 编辑界面 23 | 24 | 请安装[QtDesigner](https://build-system.fman.io/qt-designer-download)编辑文件 `ui/autoqueue.ui`。 25 | 编辑完成后,运行如下命令生成 Python 代码: 26 | 27 | ``` 28 | pyuic5 ui\autoqueue.ui > ui\autoqueue.py 29 | ``` 30 | 31 | 32 | ## 使用 33 | 34 | 执行下述命令启动。 35 | 36 | ``` 37 | D:\dev\autoqueue> (.env) python autoqueue.py 38 | ``` 39 | 40 | 或者可以编译成.exe文件分发。 41 | 42 | ``` 43 | D:\dev\autoqueue> (.env) auto-py-to-exe 44 | ``` 45 | 46 | -------------------------------------------------------------------------------- /appctrl/__init__.py: -------------------------------------------------------------------------------- 1 | import win32gui 2 | 3 | class AppCtrl(object): 4 | 5 | classname = None 6 | title = None 7 | 8 | def __init__(self): 9 | self.hWnd = self.getWindow() 10 | 11 | def getWindow(self): 12 | assert(self.classname is not None or self.title is not None) 13 | return win32gui.FindWindow(self.classname, self.title) 14 | 15 | def getRect(self): 16 | return win32gui.GetWindowRect(self.hWnd) 17 | 18 | def activate(self): 19 | win32gui.SetForegroundWindow(self.hWnd) 20 | -------------------------------------------------------------------------------- /appctrl/battlenet.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pyautogui as gui 3 | import win32gui 4 | 5 | from . import AppCtrl 6 | 7 | class BattleNetApp(AppCtrl): 8 | classname = 'Qt5QWindowOwnDCIcon' 9 | 10 | def start_game(self): 11 | hWnd = self.getWindow() 12 | rect = self.getRect() 13 | 14 | click_pos = (rect[0] + 320, rect[3] - 60) 15 | 16 | self.activate() 17 | gui.moveTo(*click_pos, 1) 18 | gui.click() 19 | gui.moveTo(100, 100) 20 | 21 | -------------------------------------------------------------------------------- /appctrl/wow.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pyautogui as gui 3 | from . import AppCtrl 4 | from utils.screenshot import regionMatchesColor 5 | 6 | 7 | class WowClassicApp(AppCtrl): 8 | title = "魔兽世界" 9 | 10 | def isCharacterSelect(self): 11 | """Test if in Character Select screen. 12 | """ 13 | rect = self.getRect() 14 | return regionMatchesColor((rect[0] + 857, rect[1] + 983, 22, 19), (117, 0, 0), 16) 15 | 16 | def enterGame(self): 17 | self.activate() 18 | rect = self.getRect() 19 | gui.moveTo(rect[0] + 860, rect[1] + 990, 1) 20 | gui.click() 21 | 22 | def antiIdle(self): 23 | self.activate() 24 | gui.keyDown('w') 25 | time.sleep(0.05) 26 | gui.keyUp('w') 27 | gui.keyDown('s') 28 | time.sleep(0.05) 29 | gui.keyUp('s') 30 | -------------------------------------------------------------------------------- /autoqueue.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | from datetime import datetime, timedelta 4 | from PyQt5.QtWidgets import QApplication, QDialog 5 | from PyQt5.QtCore import QThread, pyqtSignal, Qt 6 | from ui.autoqueue import Ui_AutoQueueDialog 7 | from appctrl.battlenet import BattleNetApp 8 | from appctrl.wow import WowClassicApp 9 | 10 | 11 | 12 | class AutoQueueDialog(QDialog): 13 | 14 | def __init__(self): 15 | super().__init__(None, Qt.WindowStaysOnTopHint) 16 | 17 | self.ui = Ui_AutoQueueDialog() 18 | self.ui.setupUi(self) 19 | 20 | self.ui.startTimerButton.clicked.connect(self.start_timer) 21 | self.ui.startNowButton.clicked.connect(self.start_now) 22 | self.ui.startAntiIdle.clicked.connect(self.start_anti_idle) 23 | 24 | def update_status(self, msg): 25 | self.ui.status.setText(msg) 26 | 27 | def disable_buttons(self): 28 | self.ui.start_time.setEnabled(False) 29 | self.ui.startNowButton.setEnabled(False) 30 | self.ui.startTimerButton.setEnabled(False) 31 | self.ui.startAntiIdle.setEnabled(False) 32 | 33 | def start_timer(self): 34 | # Disable button 35 | self.disable_buttons() 36 | 37 | # Calculate countdown secs 38 | start_time = datetime.combine( 39 | datetime.today(), 40 | self.ui.start_time.time().toPyTime() 41 | ) 42 | now = datetime.now() 43 | 44 | if start_time < now: 45 | start_time = start_time + timedelta(days=1) 46 | 47 | secs = (start_time - now).seconds 48 | 49 | self.thread = AutoQueueThread(secs) 50 | self.thread.signal.connect(self.update_status) 51 | self.thread.start() 52 | 53 | def start_now(self): 54 | self.disable_buttons() 55 | self.thread = AutoQueueThread(0) 56 | self.thread.signal.connect(self.update_status) 57 | self.thread.start() 58 | 59 | def start_anti_idle(self): 60 | self.disable_buttons() 61 | self.thread = AntiIdleThread() 62 | self.thread.signal.connect(self.update_status) 63 | self.thread.start() 64 | 65 | 66 | class AntiIdleThread(QThread): 67 | 68 | signal = pyqtSignal(str) 69 | 70 | def __init__(self): 71 | super().__init__() 72 | 73 | def update_status(self, msg): 74 | self.signal.emit(msg) 75 | 76 | 77 | def anti_idle(self): 78 | wow = WowClassicApp() 79 | 80 | # Anti idle 81 | anti_idle_interval = 60 82 | anti_idle_timer = anti_idle_interval 83 | while True: 84 | self.update_status('防掉线已启动,%s 秒后移动角色...' % anti_idle_timer) 85 | time.sleep(1) 86 | anti_idle_timer -= 1 87 | if anti_idle_timer == 0: 88 | wow.antiIdle() 89 | anti_idle_timer = anti_idle_interval 90 | 91 | 92 | def run(self): 93 | self.anti_idle() 94 | 95 | 96 | class AutoQueueThread(AntiIdleThread): 97 | 98 | signal = pyqtSignal(str) 99 | 100 | def __init__(self, countdown): 101 | super().__init__() 102 | self.countdown = countdown 103 | 104 | def update_status(self, msg): 105 | self.signal.emit(msg) 106 | 107 | def run(self): 108 | # Show label 109 | while self.countdown > 0: 110 | self.update_status('开始倒计时,%s 秒后启动游戏' % self.countdown) 111 | time.sleep(1) 112 | self.countdown -= 1 113 | 114 | self.update_status('正在启动游戏,30秒后检测排队...') 115 | 116 | battlenet = BattleNetApp() 117 | battlenet.start_game() 118 | 119 | time.sleep(30) 120 | 121 | wow = WowClassicApp() 122 | 123 | # Start queuing... 124 | queued_secs = 0 125 | while not wow.isCharacterSelect(): 126 | self.update_status('正在排队,已排队 %s 秒...' % queued_secs) 127 | time.sleep(1) 128 | queued_secs += 1 129 | 130 | enter_game_timer = 30 131 | while enter_game_timer > 0: 132 | self.update_status('已进入角色选择画面,%s 秒后进入游戏...' % enter_game_timer) 133 | time.sleep(1) 134 | enter_game_timer -= 1 135 | 136 | self.update_status('正在进入游戏,10秒后启动防掉线...') 137 | wow.enterGame() 138 | 139 | time.sleep(10) 140 | 141 | self.anti_idle() 142 | 143 | 144 | 145 | def create_main_window(): 146 | app = QApplication(sys.argv) 147 | window = AutoQueueDialog() 148 | window.show() 149 | 150 | sys.exit(app.exec_()) 151 | 152 | if __name__ == '__main__': 153 | create_main_window() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | auto-py-to-exe==2.6.6 2 | PyAutoGUI==0.9.48 3 | PyQt5==5.14.1 4 | pywin32==227 5 | -------------------------------------------------------------------------------- /ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charlee/wowautoqueue/5f03d3aee32b1f64a2c9361bc782914bbc9dbacb/ui/__init__.py -------------------------------------------------------------------------------- /ui/autoqueue.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'ui\autoqueue.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.14.1 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | 10 | from PyQt5 import QtCore, QtGui, QtWidgets 11 | 12 | 13 | class Ui_AutoQueueDialog(object): 14 | def setupUi(self, AutoQueueDialog): 15 | AutoQueueDialog.setObjectName("AutoQueueDialog") 16 | AutoQueueDialog.resize(314, 238) 17 | self.line = QtWidgets.QFrame(AutoQueueDialog) 18 | self.line.setGeometry(QtCore.QRect(20, 170, 271, 16)) 19 | self.line.setFrameShape(QtWidgets.QFrame.HLine) 20 | self.line.setFrameShadow(QtWidgets.QFrame.Sunken) 21 | self.line.setObjectName("line") 22 | self.layoutWidget = QtWidgets.QWidget(AutoQueueDialog) 23 | self.layoutWidget.setGeometry(QtCore.QRect(20, 190, 271, 32)) 24 | self.layoutWidget.setObjectName("layoutWidget") 25 | self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) 26 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 27 | self.verticalLayout.setObjectName("verticalLayout") 28 | self.label_2 = QtWidgets.QLabel(self.layoutWidget) 29 | self.label_2.setObjectName("label_2") 30 | self.verticalLayout.addWidget(self.label_2) 31 | self.status = QtWidgets.QLabel(self.layoutWidget) 32 | self.status.setObjectName("status") 33 | self.verticalLayout.addWidget(self.status) 34 | self.groupBox = QtWidgets.QGroupBox(AutoQueueDialog) 35 | self.groupBox.setGeometry(QtCore.QRect(10, 20, 281, 91)) 36 | self.groupBox.setObjectName("groupBox") 37 | self.widget = QtWidgets.QWidget(self.groupBox) 38 | self.widget.setGeometry(QtCore.QRect(10, 20, 257, 58)) 39 | self.widget.setObjectName("widget") 40 | self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.widget) 41 | self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) 42 | self.verticalLayout_3.setObjectName("verticalLayout_3") 43 | self.horizontalLayout = QtWidgets.QHBoxLayout() 44 | self.horizontalLayout.setObjectName("horizontalLayout") 45 | self.label = QtWidgets.QLabel(self.widget) 46 | self.label.setObjectName("label") 47 | self.horizontalLayout.addWidget(self.label) 48 | self.start_time = QtWidgets.QTimeEdit(self.widget) 49 | self.start_time.setObjectName("start_time") 50 | self.horizontalLayout.addWidget(self.start_time) 51 | self.startTimerButton = QtWidgets.QPushButton(self.widget) 52 | self.startTimerButton.setObjectName("startTimerButton") 53 | self.horizontalLayout.addWidget(self.startTimerButton) 54 | self.verticalLayout_3.addLayout(self.horizontalLayout) 55 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout() 56 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 57 | self.label_3 = QtWidgets.QLabel(self.widget) 58 | self.label_3.setObjectName("label_3") 59 | self.horizontalLayout_2.addWidget(self.label_3) 60 | self.startNowButton = QtWidgets.QPushButton(self.widget) 61 | self.startNowButton.setObjectName("startNowButton") 62 | self.horizontalLayout_2.addWidget(self.startNowButton) 63 | self.verticalLayout_3.addLayout(self.horizontalLayout_2) 64 | self.groupBox_2 = QtWidgets.QGroupBox(AutoQueueDialog) 65 | self.groupBox_2.setGeometry(QtCore.QRect(10, 110, 281, 51)) 66 | self.groupBox_2.setObjectName("groupBox_2") 67 | self.startAntiIdle = QtWidgets.QPushButton(self.groupBox_2) 68 | self.startAntiIdle.setGeometry(QtCore.QRect(10, 20, 123, 23)) 69 | self.startAntiIdle.setObjectName("startAntiIdle") 70 | 71 | self.retranslateUi(AutoQueueDialog) 72 | QtCore.QMetaObject.connectSlotsByName(AutoQueueDialog) 73 | 74 | def retranslateUi(self, AutoQueueDialog): 75 | _translate = QtCore.QCoreApplication.translate 76 | AutoQueueDialog.setWindowTitle(_translate("AutoQueueDialog", "《魔兽世界经典怀旧服》自动排队工具")) 77 | self.label_2.setText(_translate("AutoQueueDialog", "当前状态")) 78 | self.status.setText(_translate("AutoQueueDialog", "待机")) 79 | self.groupBox.setTitle(_translate("AutoQueueDialog", "自动排队")) 80 | self.label.setText(_translate("AutoQueueDialog", "选择启动游戏的时间")) 81 | self.start_time.setDisplayFormat(_translate("AutoQueueDialog", "hh:mm")) 82 | self.startTimerButton.setText(_translate("AutoQueueDialog", "定时排队")) 83 | self.label_3.setText(_translate("AutoQueueDialog", "或者不使用定时器")) 84 | self.startNowButton.setText(_translate("AutoQueueDialog", "立即排队")) 85 | self.groupBox_2.setTitle(_translate("AutoQueueDialog", "防掉线")) 86 | self.startAntiIdle.setText(_translate("AutoQueueDialog", "启动防掉线")) 87 | -------------------------------------------------------------------------------- /ui/autoqueue.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | AutoQueueDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 314 10 | 238 11 | 12 | 13 | 14 | 《魔兽世界经典怀旧服》自动排队工具 15 | 16 | 17 | 18 | 19 | 20 20 | 170 21 | 271 22 | 16 23 | 24 | 25 | 26 | Qt::Horizontal 27 | 28 | 29 | 30 | 31 | 32 | 20 33 | 190 34 | 271 35 | 32 36 | 37 | 38 | 39 | 40 | 41 | 42 | 当前状态 43 | 44 | 45 | 46 | 47 | 48 | 49 | 待机 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 10 59 | 20 60 | 281 61 | 91 62 | 63 | 64 | 65 | 自动排队 66 | 67 | 68 | 69 | 70 | 10 71 | 20 72 | 257 73 | 58 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 选择启动游戏的时间 83 | 84 | 85 | 86 | 87 | 88 | 89 | hh:mm 90 | 91 | 92 | 93 | 94 | 95 | 96 | 定时排队 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 或者不使用定时器 108 | 109 | 110 | 111 | 112 | 113 | 114 | 立即排队 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 10 127 | 110 128 | 281 129 | 51 130 | 131 | 132 | 133 | 防掉线 134 | 135 | 136 | 137 | 138 | 10 139 | 20 140 | 123 141 | 23 142 | 143 | 144 | 145 | 启动防掉线 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /utils/screenshot.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import pyautogui as gui 3 | from functools import reduce 4 | 5 | def regionMatchesColor(region, color, tolerance): 6 | """Check if the average color of the given region matches color. 7 | """ 8 | im = gui.screenshot(region=region) 9 | im_data = list(im.getdata()) 10 | 11 | count = len(im_data) 12 | total_color = reduce(lambda p1, p2: tuple(map(operator.add, p1, p2)), im_data) 13 | mean_color = (total_color[0] / count, total_color[1] / count, total_color[2] / count) 14 | 15 | color_diff = tuple(map(lambda a, b: abs(a - b), mean_color, color)) 16 | 17 | return max(color_diff) <= tolerance 18 | --------------------------------------------------------------------------------