├── README.md ├── debug.py ├── keys.json ├── main.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # 免责声明 2 | 免责声明 3 | 脚本仅供学习和研究目的使用,作者不对因使用该脚本而导致的任何后果负责。使用该脚本的风险完全由用户自行承担。 4 | 5 | 6 | 用户须知: 7 | 8 | 尽管脚本设计为非侵入性,但使用第三方工具可能违反目标平台的使用条款或服务协议。 9 | 使用该脚本可能导致账号被封禁或其他形式的处罚。 10 | 11 | 作者不保证脚本的稳定性、安全性或合法性。 12 | # DeltaForceKeyBot 13 | 三角洲行动拍卖行自动挂卡工具(单三跑刀巴克什匹配实在太久,所以利用匹配时间进行补卡),通过ocr+模拟鼠标点击实现自动购买钥匙卡 14 | 项目默认只配置了交易行>钥匙>巴克什 页面的的部分钥匙坐标数据,如有其他地图的钥匙可以将钥匙添加到收藏,然后通过debug.py 记录钥匙卡的位置来进行监控购买 15 | 16 | 17 | ## 开始 18 | ### 安装 19 | 1. 下载本代码,安装requirement.txt 20 | 2. 安装[tesseract](https://github.com/tesseract-ocr/tesseract ) 21 | 3. 下载[tesseract中文识别库](https://github.com/tesseract-ocr/tessdata) 22 | 4. 修改代码中的环境变量为本机安装的位置 23 | ``` 24 | # Tesseract 环境配置 25 | os.environ["LANGDATA_PATH"] = r"E:\Code\DeltaForce\tessdata-4.1.0\tessdata-4.1.0" 26 | os.environ["TESSDATA_PREFIX"] = r"E:\Code\DeltaForce\tessdata-4.1.0\tessdata-4.1.0" 27 | pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' 28 | ``` 29 | 30 | ## 运行 31 | ``` 32 | python main.py 33 | ``` 34 | F8开始抢卡,F9暂停抢卡,脚本已适配不同分辨率(16:9)以及多显示器的场景 35 | 开始抢卡时需要将页面点击到买卡的区域,如下图项目默认只配置了交易行>钥匙>巴克什 页面如下图的的部分钥匙坐标数据, 36 | ![image](https://github.com/user-attachments/assets/b76727bc-d126-47a5-a3ed-964f9221d38c) 37 | 38 | **如有其他地图的钥匙可以将钥匙添加到收藏,然后通过debug.py 记录钥匙卡的位置来进行监控购买** 39 | 40 | ## 其他说明 41 | ### debug.py 42 | 运行debug.py 实时获取鼠标坐标 如得到 58.21%,21.25% 则坐标应该为[0.5821,0.2125] 43 | 44 | ### keys.json 45 | ``` 46 | 47 | { 48 | "name": "巴别塔供电权限卡", #目标卡牌名称,需与游戏保持一致 49 | "base_price": 489859, #目标卡牌参考价格,该价格溢价10w内也会自动购买,不需要自行修改代码 50 | "ideal_price": 500000, #理想购买价格 51 | "position": [0.6148,0.5174], #卡牌坐标 52 | "wantBuy":1 #是否加入监控 53 | } 54 | 55 | ``` 56 | 57 | ### 购买逻辑 58 | 59 | 1. 当前价格小于理想购买价格,自动购买 60 | 2. 卡牌溢价10w以内,自动购买 61 | 3. 卡牌负溢价,自动购买 62 | 63 | 64 | ## FAQ: 65 | 1. 为什么截图区域不对 66 | > 检查自己的屏幕分辨率是否为16:9 67 | 68 | 2. ESC按键没反应/脚本无法获取截图 69 | > 使用管理员运行cmd 70 | 71 | ## Fix 72 | 1. 优化了1080p下截图过小导致ocr识别不准确的问题 73 | > 优化了二值化处理方式以及将截图等比放大两倍来提高识别准确率 74 | -------------------------------------------------------------------------------- /debug.py: -------------------------------------------------------------------------------- 1 | import pyautogui 2 | import time 3 | 4 | # 获取屏幕分辨率 5 | screen_width, screen_height = pyautogui.size() 6 | 7 | print("按 Ctrl+C 结束程序") 8 | try: 9 | while True: 10 | x, y = pyautogui.position() 11 | 12 | # 计算百分比(保留4位小数,例如 0.7523 表示 75.23%) 13 | x_percent = round(x / screen_width, 4) 14 | y_percent = round(y / screen_height, 4) 15 | 16 | # 实时显示原始坐标和百分比坐标 17 | print( 18 | f"原始坐标: X={x:<4} Y={y:<4} | " 19 | f"百分比坐标: X={x_percent:.2%} Y={y_percent:.2%}", 20 | end="\r" 21 | ) 22 | time.sleep(0.1) 23 | except KeyboardInterrupt: 24 | print("\n程序已终止") -------------------------------------------------------------------------------- /keys.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "name": "巴别塔供电权限卡", 5 | "base_price": 489859, 6 | "ideal_price": 500000, 7 | "position": [0.6148,0.5174], 8 | "wantBuy":1 9 | }, 10 | { 11 | "name": "1号审讯室", 12 | "base_price": 889019, 13 | "ideal_price": 960000, 14 | "position": [0.8641,0.5014], 15 | "wantBuy":0 16 | },{ 17 | "name": "医疗会议室", 18 | "base_price": 1130221, 19 | "ideal_price": 1240000, 20 | "position": [0.3332,0.4924], 21 | "wantBuy":0 22 | },{ 23 | "name": "Relink植入手术室", 24 | "base_price": 1550585, 25 | "ideal_price": 1550585, 26 | "position": [0.3617,0.3778], 27 | "wantBuy":0 28 | },{ 29 | "name": "总裁会议室", 30 | "base_price": 2686739, 31 | "ideal_price": 2680000, 32 | "position": [0.5871, 0.2069], 33 | "wantBuy":1 34 | },{ 35 | "name": "地下金库储藏间", 36 | "base_price": 2893131, 37 | "ideal_price": 2900000, 38 | "position": [0.368, 0.2229], 39 | "wantBuy":0 40 | },{ 41 | "name": "旅店用餐间", 42 | "base_price": 2072018, 43 | "ideal_price": 2100000, 44 | "position": [0.798,0.2111], 45 | "wantBuy":0 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pyautogui 3 | import cv2 4 | import numpy as np 5 | import time 6 | import pytesseract 7 | import os 8 | import keyboard # 用于监听键盘事件 9 | 10 | 11 | 12 | # 配置部分 13 | CONFIG_FILE = 'keys.json' 14 | 15 | # Tesseract 环境配置 16 | os.environ["LANGDATA_PATH"] = r"E:\Code\DeltaForce\tessdata-4.1.0\tessdata-4.1.0" 17 | os.environ["TESSDATA_PREFIX"] = r"E:\Code\DeltaForce\tessdata-4.1.0\tessdata-4.1.0" 18 | pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' 19 | 20 | # 全局变量 21 | keys_config = None 22 | is_running = False # 控制循环是否运行 23 | is_paused = False # 控制循环是否暂停 24 | screen_width, screen_height = pyautogui.size() 25 | 26 | def load_keys_config(): 27 | """加载钥匙价格配置文件(只读取一次)""" 28 | global keys_config 29 | if keys_config is not None: 30 | return keys_config # 如果已经加载过,直接返回 31 | 32 | try: 33 | with open(CONFIG_FILE, 'r', encoding='utf-8') as f: 34 | config = json.load(f) 35 | keys_config = config.get('keys', []) 36 | return keys_config 37 | except FileNotFoundError: 38 | print(f"[错误] 配置文件 {CONFIG_FILE} 不存在") 39 | return [] 40 | except json.JSONDecodeError: 41 | print(f"[错误] 配置文件 {CONFIG_FILE} 格式错误") 42 | return [] 43 | except Exception as e: 44 | print(f"[错误] 读取配置时发生未知错误: {str(e)}") 45 | return [] 46 | 47 | def take_screenshot(region): 48 | """截取指定区域的截图并二值化""" 49 | # 1. 截取屏幕 50 | screenshot = pyautogui.screenshot(region=region) 51 | 52 | # 2. 转换为OpenCV格式 53 | screenshot_np = np.array(screenshot) 54 | screenshot_bgr = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR) 55 | 56 | # 3. 转为灰度图 57 | gray = cv2.cvtColor(screenshot_bgr, cv2.COLOR_BGR2GRAY) 58 | 59 | # 4. 去噪 60 | denoised = cv2.fastNlMeansDenoising( 61 | gray, 62 | h=15, 63 | templateWindowSize=7, 64 | searchWindowSize=21 65 | ) 66 | scale_percent = 200 # 放大200% 67 | width = int(denoised.shape[1] * scale_percent / 100) 68 | height = int(denoised.shape[0] * scale_percent / 100) 69 | resized = cv2.resize(denoised, (width, height), interpolation=cv2.INTER_CUBIC) 70 | 71 | return resized 72 | 73 | def getCardPrice(): 74 | """获取当前卡片价格""" 75 | region_width = int(screen_width * 0.1) # 区域宽度 76 | region_height = int(screen_height * 0.05) # 区域高度 77 | region_left = int(screen_width * 0.155) #截图左上角价格坐标 78 | region_top = int(screen_height * 0.15) 79 | region = (region_left, region_top, region_width, region_height) 80 | 81 | screenshot = take_screenshot(region=region) 82 | # cv2.imwrite("./card_price.png", screenshot) # 保存图片到本地 83 | text = pytesseract.image_to_string(screenshot, lang='eng', config="--psm 6 --oem 3 -c tessedit_char_whitelist=0123456789,") 84 | 85 | try: 86 | price = int(text.replace(",", "").strip()) 87 | print(f"提取的价格文本: {price}") 88 | return price 89 | except ValueError: 90 | print("无法解析价格") 91 | return None 92 | 93 | def getCardName(): 94 | """获取当前卡片名称""" 95 | screen_width, screen_height = pyautogui.size() 96 | region_width = int(screen_width * 0.1) # 区域宽度 97 | region_height = int(screen_height * 0.03) # 区域高度 98 | region_left = int(screen_width * 0.768) 99 | region_top = int(screen_height * 0.145) 100 | region = (region_left, region_top, region_width, region_height) 101 | 102 | screenshot = take_screenshot(region=region) 103 | # cv2.imwrite("./card_name.png", screenshot) #保存图片到本地 104 | text = pytesseract.image_to_string(screenshot, lang='chi_sim', config="--psm 10") 105 | text = text.replace(" ", "").strip() # 清理空格和换行符 106 | print(f"提取的卡片名称: {text}") 107 | return text 108 | 109 | def price_check_flow(card_info): 110 | """价格检查主流程""" 111 | global is_paused 112 | 113 | # 移动到卡牌位置并点击 114 | position = card_info.get('position') 115 | pyautogui.moveTo(position[0]*screen_width, position[1]*screen_height) 116 | pyautogui.click() 117 | time.sleep(0.1) 118 | 119 | try: 120 | card_name = getCardName().strip() 121 | current_price = getCardPrice() 122 | if current_price is None: 123 | print("无法获取有效价格,跳过本次检查") 124 | pyautogui.press('esc') # 按 Esc 取消 125 | return False 126 | except Exception as e: 127 | print(f"获取门卡信息失败: {str(e)}") 128 | pyautogui.press('esc') # 按 Esc 取消 129 | return False 130 | 131 | base_price = card_info.get('base_price', 0) 132 | ideal_price = card_info.get('ideal_price', base_price) 133 | max_price = card_info.get('base_price')*1.1 # 最高溢价 10% 134 | premium = ((current_price / base_price) - 1) * 100 135 | 136 | check_card_name = card_info.get("name") 137 | print(f"当前门卡:{card_name}\n需要购买的卡:{check_card_name}") 138 | if card_name != check_card_name: 139 | pyautogui.press('esc') 140 | print("需要购买的卡与点击的卡不符,已返回上一层") 141 | return False 142 | print(f"基准价格: {base_price} | 理想价格: {ideal_price} | 当前价格/溢价: {current_price} ,{premium:.2f}% | 最高溢价:{max_price}") 143 | if premium < 0 or current_price < ideal_price or current_price - base_price <=100000: 144 | pyautogui.moveTo(screen_width*0.825,screen_height*0.852) 145 | pyautogui.click() # 再次点击确认购买 146 | print(f"[+]已自动购买{card_name},价格为:{current_price},溢价:{premium:.2f}") 147 | pyautogui.press('esc') 148 | return True 149 | else: 150 | print(">> 价格过高,重新刷新价格 <<\n\n") 151 | pyautogui.press('esc') # 按 Esc 取消 152 | return False 153 | 154 | def start_loop(): 155 | """开始循环""" 156 | global is_running, is_paused 157 | is_running = True 158 | is_paused = False 159 | print("循环已启动") 160 | 161 | def stop_loop(): 162 | """停止循环""" 163 | global is_running, is_paused 164 | is_running = False 165 | is_paused = False 166 | print("循环已停止") 167 | 168 | def main(): 169 | # getCardPrice() #debug的时候使用 170 | # getCardName() #debug的时候使用 171 | global is_running, is_paused 172 | 173 | # 初始化时加载配置 174 | keys_config = load_keys_config() 175 | if not keys_config: 176 | print("无法加载配置文件,程序退出") 177 | return 178 | 179 | # 过滤出需要购买的卡牌 180 | cards_to_buy = [card for card in keys_config if card.get('wantBuy', 0) == 1] 181 | if not cards_to_buy: 182 | print("没有需要购买的门卡,程序退出") 183 | return 184 | for card in cards_to_buy: 185 | print(f"当前需要购买: {card['name']}") 186 | # 监听键盘事件 187 | keyboard.add_hotkey('f8', start_loop) # 按 F8 开始循环 188 | keyboard.add_hotkey('f9', stop_loop) # 按 F9 停止循环 189 | 190 | print("按 F8 开始循环,按 F9 停止循环") 191 | 192 | while True: 193 | if is_running and not is_paused: 194 | for card_info in cards_to_buy: 195 | if not is_running: 196 | break 197 | print(f"正在检查门卡: {card_info['name']}") 198 | if price_check_flow(card_info): 199 | cards_to_buy.remove(card_info) 200 | print(f"剩余购买队列:{cards_to_buy}") 201 | continue 202 | time.sleep(0.1) 203 | elif is_paused: 204 | print("循环已暂停,等待手动恢复...") 205 | time.sleep(1) 206 | else: 207 | time.sleep(0.1) # 空闲时降低 CPU 占用 208 | 209 | if __name__ == "__main__": 210 | main() 211 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | keyboard==0.13.5 2 | numpy==1.24.4 3 | opencv_contrib_python==4.11.0.86 4 | opencv_python==4.11.0.86 5 | opencv_python_headless==4.11.0.86 6 | PyAutoGUI==0.9.54 7 | pytesseract==0.3.13 8 | --------------------------------------------------------------------------------