├── defaultArrow.txt ├── icon.ico ├── icon.png ├── arrow ├── 1.bmp ├── 3.bmp ├── 5.bmp ├── 6.bmp ├── d (1).bmp ├── d (2).bmp ├── d (3).bmp ├── d (4).bmp ├── d (5).bmp ├── d (6).bmp ├── d (7).bmp ├── d (8).bmp ├── s (1).bmp ├── s (2).bmp ├── s (3).bmp ├── s (4).bmp ├── s (5).bmp ├── s (6).bmp ├── s (7).bmp ├── s (8).bmp ├── w (1).bmp ├── w (10).bmp ├── w (11).bmp ├── w (12).bmp ├── w (2).bmp ├── w (3).bmp ├── w (4).bmp ├── w (5).bmp ├── w (6).bmp ├── w (7).bmp ├── w (8).bmp └── w (9).bmp ├── static ├── favicon.ico ├── index.html └── webctl.html ├── requirements.txt ├── util ├── Util.py ├── SystemTrayIcon.py ├── webui.py ├── loadSetting.py ├── globalHotKeyManager.py ├── imageProcessing.py └── settingGUI.py ├── README.md ├── app.py └── LICENSE /defaultArrow.txt: -------------------------------------------------------------------------------- 1 | WSDAW -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/icon.ico -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/icon.png -------------------------------------------------------------------------------- /arrow/1.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/1.bmp -------------------------------------------------------------------------------- /arrow/3.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/3.bmp -------------------------------------------------------------------------------- /arrow/5.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/5.bmp -------------------------------------------------------------------------------- /arrow/6.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/6.bmp -------------------------------------------------------------------------------- /arrow/d (1).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/d (1).bmp -------------------------------------------------------------------------------- /arrow/d (2).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/d (2).bmp -------------------------------------------------------------------------------- /arrow/d (3).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/d (3).bmp -------------------------------------------------------------------------------- /arrow/d (4).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/d (4).bmp -------------------------------------------------------------------------------- /arrow/d (5).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/d (5).bmp -------------------------------------------------------------------------------- /arrow/d (6).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/d (6).bmp -------------------------------------------------------------------------------- /arrow/d (7).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/d (7).bmp -------------------------------------------------------------------------------- /arrow/d (8).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/d (8).bmp -------------------------------------------------------------------------------- /arrow/s (1).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/s (1).bmp -------------------------------------------------------------------------------- /arrow/s (2).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/s (2).bmp -------------------------------------------------------------------------------- /arrow/s (3).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/s (3).bmp -------------------------------------------------------------------------------- /arrow/s (4).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/s (4).bmp -------------------------------------------------------------------------------- /arrow/s (5).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/s (5).bmp -------------------------------------------------------------------------------- /arrow/s (6).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/s (6).bmp -------------------------------------------------------------------------------- /arrow/s (7).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/s (7).bmp -------------------------------------------------------------------------------- /arrow/s (8).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/s (8).bmp -------------------------------------------------------------------------------- /arrow/w (1).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/w (1).bmp -------------------------------------------------------------------------------- /arrow/w (10).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/w (10).bmp -------------------------------------------------------------------------------- /arrow/w (11).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/w (11).bmp -------------------------------------------------------------------------------- /arrow/w (12).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/w (12).bmp -------------------------------------------------------------------------------- /arrow/w (2).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/w (2).bmp -------------------------------------------------------------------------------- /arrow/w (3).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/w (3).bmp -------------------------------------------------------------------------------- /arrow/w (4).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/w (4).bmp -------------------------------------------------------------------------------- /arrow/w (5).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/w (5).bmp -------------------------------------------------------------------------------- /arrow/w (6).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/w (6).bmp -------------------------------------------------------------------------------- /arrow/w (7).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/w (7).bmp -------------------------------------------------------------------------------- /arrow/w (8).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/w (8).bmp -------------------------------------------------------------------------------- /arrow/w (9).bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/arrow/w (9).bmp -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDNDZZK/helldivers2AutoStratagems/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow==11.1.0 2 | mss==10.0.0 3 | pynput==1.8.1 4 | PyQt6==6.9.0 5 | PyQt6_sip==13.10.0 6 | pystray==0.19.5 7 | fastapi==0.115.12 8 | uvicorn==0.34.2 9 | python-multipart==0.0.20 -------------------------------------------------------------------------------- /util/Util.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | 4 | def run_in_thread(func): 5 | def wrapper(*args, **kwargs) -> threading.Thread: 6 | thread = threading.Thread(target=func, args=args, kwargs=kwargs) 7 | thread.start() 8 | return thread 9 | return wrapper -------------------------------------------------------------------------------- /util/SystemTrayIcon.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pystray import Icon as PystrayIcon, Menu as PystrayMenu, MenuItem as PystrayMenuItem 3 | from PIL import Image 4 | from util.Util import run_in_thread 5 | from util.settingGUI import settingsGUI 6 | 7 | 8 | class SystemTrayIcon: 9 | def __init__(self, settingsGUI: settingsGUI, start_webuiFunc, stop_webuiFunc, image_path='./icon.png'): 10 | self.icon_image = Image.open(image_path) 11 | self.settingsGUI = settingsGUI 12 | self.start_webuiFunc = start_webuiFunc 13 | self.stop_webuiFunc = stop_webuiFunc 14 | self.icon = PystrayIcon('keyboardControlMouse', self.icon_image, 15 | 'keyboardControlMouse', self.create_menu('start webui')) 16 | 17 | def create_menu(self, action): 18 | if action == 'start webui': 19 | return PystrayMenu( 20 | PystrayMenuItem('settings', action=self.settingsGUI.open_settings_gui), 21 | PystrayMenuItem('start webui', action=self.on_start_webui), 22 | PystrayMenuItem('exit', action=self.on_exit), 23 | ) 24 | elif action == 'stop webui': 25 | return PystrayMenu( 26 | PystrayMenuItem('settings', action=self.settingsGUI.open_settings_gui), 27 | PystrayMenuItem('stop webui', action=self.on_stop_webui), 28 | PystrayMenuItem('exit', action=self.on_exit), 29 | ) 30 | 31 | @run_in_thread 32 | def start(self, extra_execution_function=[]): 33 | self.icon.run() 34 | for func in extra_execution_function: 35 | func() 36 | 37 | def on_start_webui(self): 38 | self.start_webuiFunc() 39 | self.icon.menu = self.create_menu('stop webui') 40 | 41 | def on_stop_webui(self): 42 | self.stop_webuiFunc() 43 | self.icon.menu = self.create_menu('start webui') 44 | 45 | def on_exit(self): 46 | logging.debug('exit触发') 47 | self.icon.stop() 48 | 49 | def change_icon(self, image): 50 | """ 51 | 改变任务栏图标 52 | """ 53 | self.icon.icon = image -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # helldivers2AutoStratagems 2 | 3 | [![GitHub license](https://img.shields.io/github/license/GDNDZZK/helldivers2AutoStratagems.svg)](https://github.com/GDNDZZK/helldivers2AutoStratagems/blob/master/LICENSE) ![Python版本](https://img.shields.io/badge/python-3.10%2B-yellow) ![GitHub issues](https://img.shields.io/github/issues/GDNDZZK/helldivers2AutoStratagems.svg) ![GitHub all releases](https://img.shields.io/github/downloads/GDNDZZK/helldivers2AutoStratagems/total.svg) ![GitHub forks](https://img.shields.io/github/forks/GDNDZZK/helldivers2AutoStratagems.svg?style=flat) 4 | ![GitHub stars](https://img.shields.io/github/stars/GDNDZZK/helldivers2AutoStratagems.svg?style=flat) 5 | 6 | 7 | 绝地潜兵2一键搓球!基于视觉识别一键自动更新战略配备指令,附带Web UI,可以使用快捷键或网页搓球. 8 | 9 | [演示视频(使用快捷键呼叫战备)](https://www.acfun.cn/v/ac47131715) [演示视频(使用Web UI呼叫战备)](https://www.acfun.cn/v/ac47189365) 10 | 11 | ## 注意 12 | 13 | - 暂不支持弧形UI 14 | - 暂不支持HDR(你可以手动设置`COLORS`参数临时支持,但这种方法麻烦且不通用.如果你有办法获取HDR截图并正确转换到SDR,欢迎修改`imageProcessing.capture_screenshot`函数并提交PR) 15 | - 建议在游戏中使用 `按住`打开战略配备面板 16 | - 本软件使用[GPL-3.0](https://github.com/GDNDZZK/helldivers2AutoStratagems/blob/master/LICENSE)开源,请民主的使用 17 | - 有任何问题欢迎提issues,欢迎提PR参与开发 18 | 19 | ## 运行方法 20 | 21 | #### 1.使用Releases版本 22 | 23 | 1. 下载并解压7z压缩包 24 | 2. 运行程序: 25 | 26 | ``` 27 | helldivers2AutoStratagems.exe 28 | ``` 29 | 3. 运行后会出现托盘图标,右键点击 `exit`退出 30 | 31 | #### 2.从源代码构建 32 | 33 | 1. 克隆或下载此仓库到本地 34 | 2. 确保你的Python版本在3.10及以上 35 | 3. 安装必要的Python库: 36 | 37 | ```shell 38 | pip install -r requirements.txt 39 | ``` 40 | 4. 运行程序: 41 | 42 | ``` 43 | app.py 44 | ``` 45 | 5. 运行后会出现托盘图标,右键点击 `exit`退出 46 | 47 | ## 使用GUI自定义设置 48 | 49 | #### 如何打开设置面板 50 | 51 | * 启动程序时会自动打开设置面板,如果你不希望这样可以取消勾选 `允许设置面板随程序开启`. 52 | * 右键托盘图标,点击 `settings` 53 | * 使用快捷键(默认 `+<=>`) 54 | 55 | #### 识别区域设置 56 | 57 | 1. 点击 `交互式更改`会出现区域选择框 58 | 2. 按住并拖动区域选择框右下角区域(`完成`按钮附近)改变大小 59 | 3. 按住并拖动区域选择框的其它区域移动 60 | 4. 点击 `截图测试`可以看到识别区域是否正确 61 | 62 | #### WebUI设置 63 | 64 | 1. 左侧输入框代表监听地址,默认`all`监听所有地址(ipv4和ipv6),你可以设置为`0.0.0.0`仅监听ipv4地址,`::`仅监听ipv6地址,`127.0.0.1`仅监听本机 65 | 2. 右侧输入框代表端口,默认`80`,如果有冲突可以设置为其它端口 66 | 3. 如果你已经启动了WebUI,需要重新打开才能生效 67 | 68 | #### 按键设置 69 | 70 | 1. 点击`配置键盘快捷键`,打开快捷键设置面板 71 | 2. 点击对应按键的按钮(例如`识别按键`、`打开设置`等)会弹出按键输入框 72 | 3. 按下按键会显示,松开后会记录 73 | 4. `战略配备键位`(包括`上`,`下`,`左`,`右`,`战备面板`)只允许设置一个按键,这些按键用于输出 74 | 75 | ## 识别战备 76 | 77 | 1. 按下快捷键(默认` + <->`)开始识别 78 | 2. 两声提示音表示更新完成,如果你开启了Web UI,更新会自动同步 79 | 3. 听到长提示音表示识别错误,建议找光线较暗的地方重新识别 80 | 81 | ## 使用快捷键呼叫战备 82 | 83 | #### 标准模式 84 | 85 | 1. 默认使用`+<1>`,`+<2>`...`+<9>`,`+<0>`呼叫战备面板中第1-10个战备 86 | 2. 标准模式不会实时更新战备,因此战备有变动要重新识别才能同步 87 | 88 | #### 干扰器优化模式 89 | 1. 每次使用战备都会重新识别,不会输出中间文件 90 | 2. 此模式不会实时更新设置,因此修改设置可能要重新启动软件才能生效 91 | 3. 由于每次使用战备都会重新识别,所以还是会比识别完成后的标准模式慢,但会比先识别后使用的标准模式快 92 | 4. 默认使用`+`...`+`呼叫战备面板中第1-10个战备 93 | 5. 实测效果其实一般,只有一定的概率能在指令改变前召唤出来,并且有可能因为指令变化召唤出其它战备.所以其实并不推荐用这个,遇到干扰器还是建议老老实实手搓 94 | 95 | ## 使用网页呼叫战备(Web UI) 96 | 97 | 1. 右键托盘图标,点击`start webui`开启Web UI 98 | 2. 如果你缺少网络基础知识,请使用搜索引擎搜索`查询电脑的局域网ip`,确保你想用来显示网页的设备和你的电脑连接了相同`局域网` 99 | 3. 在浏览器中访问`http://你电脑的ip:端口号`打开网页,如果端口号是80可以不用输入(http默认端口) 100 | 4. 一些示例(注意这只是示例,请填写你电脑实际的地址和你设置的端口号):`http://192.168.1.2:2333`(ipv4局域网地址,使用2333端口),`http://192.168.1.2`(使用默认80端口),`http://[fe80::a1b2:c3d4:e5f6:789a]:5555`(ipv6局域网地址,使用5555端口),`http://[fe80::a1b2:c3d4:e5f6:789a]`(ipv6局域网地址,使用默认80端口) 101 | 5. 注意部分浏览器可能会强制将`http`替换为`https`导致无法使用,如有这种情况请自行解决 102 | 6. 网页支持横屏和竖屏,支持PC和移动端,支持鼠标和触屏 103 | 7. 打开网页后识别出新的战备会自动添加到网页上,鼠标悬停或触屏长按网页上的战备框右上角会出现删除按钮,点击可以移除网页中单个战备 104 | 8. 点击即可自动打开战备面板呼叫对应战备 105 | 9. 刷新网页移除网页中的所有战备 106 | 10. 设计时并未考虑同时打开多个网页的情况,如果你这样做可能会遇到非预期异常现象 107 | 108 | ## 部分文件和目录的说明与配置 109 | 110 | #### temp/ 111 | 112 | 1. 此目录仅在更新过战略配备指令后才会出现 113 | 2. 识别后的原始结果会存入 `./temp/arrow_original.txt`,可用于评估识别效果 114 | 3. 如果 `./temp/arrow.txt`存在,使用`标准呼叫战备按键`会自动读取该文件中的战略配备指令,你可以手动修改此文件改变`标准呼叫战备按键`触发的指令(重新识别后该文件会被覆盖,因此不推荐这样做) 115 | 116 | #### defaultArrow.txt 117 | 118 | 1. 这里可以设置战略配备指令默认值,当某个战备因为冷却等原因无法识别时会使用此文件下定义的指令(如果存在) 119 | 2. 如果 `./temp/arrow.txt`不存在,按下`标准呼叫战备按键`后会读取该文件 120 | 3. `defaultArrow.txt`、`./temp/arrow.txt`、`./temp/arrow_original.txt`中第N行对应第N个战备 121 | 122 | #### config.ini 123 | 124 | 1. 如果文件不存在,将会使用默认值自动创建 125 | 2. 所有设置会被保存在这里 126 | 3. `THRESHOLD`和 `COLORS`通常不建议自行修改,假如你使用了滤镜等改变画面颜色导致识别失败,可以尝试修改这两项参数.过滤效果可以查看 `./temp/screenshot_binary.bmp` 127 | 4. `COLORS`代表二值化保留的颜色,`THRESHOLD`代表阈值 128 | 5. 设置`COLORS`参数你可以获取不同战备边框和战备箭头的颜色16进制RGB代码,多种颜色使用`,`分隔 129 | 130 | #### arrow/ 131 | 132 | 1. 此目录存放图像匹配模板,通常不建议修改 133 | 2. 此目录下任何修改需要重启软件后生效 134 | 3. 当你发现某个战备指令方向识别错误时可以尝试去 `./temp/split_images/{num}/`下找到该箭头图片,将箭头方向改为向上后放入 `./arrow`下,也许能改善识别效果 135 | 4. 过多的模板图片会降低识别速度,还可能降低识别效果 136 | 137 | ## TODO 138 | 139 | * [X] 获取截图 140 | * [X] 统一大小 141 | * [X] 二值化 142 | * [X] 获取指令区域 143 | * [X] 指令识别 144 | * [X] 快捷键 145 | * [X] 默认值 146 | * [X] 指令输入 147 | * [X] 托盘图标 148 | * [X] 提示音频 149 | * [X] 使用config.ini调参 150 | * [X] 可设置的按键触发速度 151 | * [X] 带鱼屏导致缩放后战备面板过小 #2 152 | * [X] 虚线边框的战备无法识别 #3 153 | * [X] 干扰器优化模式(每次按下战备先识别后触发,不等待输出临时文件提高识别速度) 154 | * [X] 可设置的按键(小键盘问题 #3) 155 | * [ ] UI弧度修正(感觉好麻烦,摸了) 156 | * [ ] HDR自动适配 157 | * [ ] 预设战备字典 158 | * [X] 实时显示状态可交互的网页(咕咕咕咕) 159 | * [ ] Linux X11适配(Wayland存在诸多问题无法解决,暂时放弃.蔚蓝得毁掉一切😭) 160 | 161 | ## settingGUI TODO 162 | 163 | * [X] 完善按键输入识别,支持区分左右modifies 164 | * [X] 添加打开战备面板(ACTIVATION)按键设置项 165 | * [X] 添加webui设置项 166 | * [ ] 添加二值化设置项 167 | * [ ] 让按键输入识别在Wayland下工作(X11情况未知,需要测试) 168 | * [ ] 修复wayland下resizePanel无法获取窗口位置的问题(大概差不多可能是修不了,蔚蓝得毁掉一切😭) 169 | -------------------------------------------------------------------------------- /util/webui.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import threading 3 | import time 4 | 5 | from fastapi.responses import RedirectResponse 6 | from fastapi.staticfiles import StaticFiles 7 | import uvicorn 8 | from fastapi import FastAPI, Form 9 | 10 | from util.globalHotKeyManager import c 11 | from util.loadSetting import getConfigDict 12 | import logging 13 | from pathlib import Path 14 | from logging import Filter 15 | import re 16 | 17 | if getattr(sys, 'frozen', False): 18 | # 打包后重定向日志到文件 19 | log_path = Path(sys.executable).parent / 'webui.log' 20 | sys.stdout = open(log_path, 'w') 21 | sys.stderr = sys.stdout 22 | logging.basicConfig(stream=sys.stdout, level=logging.INFO) 23 | 24 | 25 | class EndpointFilter(Filter): 26 | """过滤指定端点的访问日志""" 27 | def __init__(self, excluded_endpoints: list): 28 | super().__init__() 29 | self.excluded_endpoints = excluded_endpoints 30 | # 匹配日志中的请求方法和路径 31 | self.pattern = re.compile( 32 | r'"(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\s+([^\s?]+)' 33 | ) 34 | 35 | def filter(self, record: logging.LogRecord) -> bool: 36 | msg = record.getMessage() 37 | match = self.pattern.search(msg) 38 | if match: 39 | method, path = match.groups() 40 | path = path.rstrip('/') or '/' # 标准化路径 41 | if path in self.excluded_endpoints: 42 | return False 43 | return True 44 | 45 | class FastAPIServer: 46 | def __init__(self): 47 | 48 | self.code_list = [] 49 | 50 | # 创建 FastAPI 应用 51 | self.app = FastAPI() 52 | self.app.mount("/static/", StaticFiles(directory="./static"), name="static") 53 | 54 | @self.app.get("/test") 55 | async def test(): 56 | return "OK" 57 | 58 | # 定义 / 路由,返回 index.html 59 | @self.app.get("/") 60 | async def index(): 61 | return RedirectResponse(url="index.html") 62 | 63 | @self.app.get("/index.html") 64 | async def index_html(): 65 | return RedirectResponse(url="static/index.html") 66 | 67 | @self.app.get("/webctl.html") 68 | async def webctl(): 69 | return RedirectResponse(url="static/webctl.html") 70 | 71 | @self.app.get("/webctl") 72 | async def index_webctl(): 73 | return RedirectResponse(url="/webctl.html") 74 | 75 | @self.app.get("/favicon.ico") 76 | async def favicon(): 77 | return RedirectResponse("static/favicon.ico") 78 | 79 | @self.app.get('/code') 80 | async def code(): 81 | ''' 82 | code_list: [{'code' : 'WASD', 'imgUrl' : 'data:image/png;base64,xxx','codeImgUrl' : 'data:image/png;base64,xxx'}},...,{...}] 83 | return: 84 | ```json 85 | { 86 | "code" : 0, 87 | "data" : code_list 88 | } 89 | ``` 90 | ''' 91 | result_dict = {'code': 0, 'data': self.code_list.copy()} 92 | self.code_list = [] 93 | return result_dict 94 | 95 | @self.app.post('/exec') 96 | async def exec(line_s:str = Form() ): 97 | # 传入参数字符串line_s 98 | try: 99 | c(line_s,activation=True) 100 | except Exception as e: 101 | return {'code': 1, 'msg': str(e)} 102 | return {'code': 0} 103 | 104 | # 服务器实例和线程引用 105 | self._server = None 106 | self._thread = None 107 | # 添加日志过滤配置 108 | self._configure_access_log_filter() 109 | 110 | def _configure_access_log_filter(self): 111 | """配置需要排除的接口路径""" 112 | excluded_endpoints = [ 113 | "/code", 114 | ] 115 | 116 | uvicorn_access_logger = logging.getLogger("uvicorn.access") 117 | # 清理旧过滤器 118 | for f in uvicorn_access_logger.filters[:]: 119 | if isinstance(f, EndpointFilter): 120 | uvicorn_access_logger.removeFilter(f) 121 | # 添加新过滤器 122 | uvicorn_access_logger.addFilter(EndpointFilter(excluded_endpoints)) 123 | 124 | def set_code_list(self, new_code_list : list): 125 | self.code_list = new_code_list 126 | 127 | def start(self): 128 | """ 129 | 启动服务 130 | 如果已经启动,则不重复启动 131 | """ 132 | config_dict = getConfigDict() 133 | if self._server is not None and self._thread is not None and self._thread.is_alive(): 134 | print( 135 | f"Server is already running on port {self._server.config.port}") 136 | return 137 | 138 | # 配置 uvicorn 139 | config = uvicorn.Config( 140 | app=self.app, 141 | host=config_dict['WEB_GUI_HOST'] if config_dict['WEB_GUI_HOST'] and config_dict['WEB_GUI_HOST'].upper() != 'ALL' else None, 142 | port=int(float(config_dict['WEB_GUI_PORT'])), 143 | log_level="info", 144 | lifespan="on", 145 | ) 146 | self._server = uvicorn.Server(config) 147 | 148 | # 在独立线程中运行 149 | def _run(): 150 | self._server.run() 151 | 152 | self._thread = threading.Thread(target=_run, daemon=True) 153 | self._thread.start() 154 | 155 | # 等待服务器启动 156 | while not self._server.started: 157 | time.sleep(0.01) 158 | print(f"Server started on {config_dict['WEB_GUI_HOST']}:{int(float(config_dict['WEB_GUI_PORT']))}") 159 | 160 | def stop(self): 161 | """ 162 | 关闭服务,释放资源 163 | 关闭后可再次 start 164 | """ 165 | if self._server is None: 166 | print("Server is not running.") 167 | return 168 | 169 | # 通知服务器退出 170 | self._server.should_exit = True 171 | # 等待线程结束 172 | if self._thread is not None: 173 | self._thread.join() 174 | 175 | print("Server stopped.") 176 | 177 | # 重置引用,允许再次启动 178 | self._server = None 179 | self._thread = None 180 | -------------------------------------------------------------------------------- /util/loadSetting.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | 4 | default_config = r'''; encoding: utf-8 5 | ; 按键设置 6 | ; 可选按键 7 | ; '', '', '', '', '', '', '', '', 8 | ; '', '', '', '', '', '', '', '', '', 9 | ; '', '', '', '', '', '', '', '', '', '', '', 10 | ; '', '', '', '', '', '', '', '', '', '', 11 | ; '', '', '', '', '', '', '', '', '', '', 12 | ; '', '', '', 13 | ; '', '', '', '', 14 | ; '', '', '', '', '', 15 | ; '', '', '', '', '', '', '', '', '', 16 | ; '', '', '', '', '', '', '', '', '', '
227 | 228 | 432 | 433 | 434 | -------------------------------------------------------------------------------- /util/globalHotKeyManager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | import threading 4 | import random 5 | from pynput import keyboard 6 | from pynput.keyboard import Key, KeyCode, Controller 7 | from util.loadSetting import getConfigDict 8 | 9 | from util.Util import run_in_thread 10 | key_dict = { 11 | '': Key.alt, 12 | '': Key.alt_l, 13 | '': Key.alt_r, 14 | '': Key.alt_gr, 15 | '': Key.backspace, 16 | '': Key.caps_lock, 17 | '': Key.cmd, 18 | '': Key.cmd_l, 19 | '': Key.cmd_r, 20 | '': Key.ctrl, 21 | '': Key.ctrl_l, 22 | '': Key.ctrl_r, 23 | '': Key.delete, 24 | '': Key.down, 25 | '': Key.end, 26 | '': Key.enter, 27 | '': Key.esc, 28 | '': Key.f1, 29 | '': Key.f2, 30 | '': Key.f3, 31 | '': Key.f4, 32 | '': Key.f5, 33 | '': Key.f6, 34 | '': Key.f7, 35 | '': Key.f8, 36 | '': Key.f9, 37 | '': Key.f10, 38 | '': Key.f11, 39 | '': Key.f12, 40 | '': Key.f13, 41 | '': Key.f14, 42 | '': Key.f15, 43 | '': Key.f16, 44 | '': Key.f17, 45 | '': Key.f18, 46 | '': Key.f19, 47 | '': Key.f20, 48 | '': Key.home, 49 | '': Key.left, 50 | '': Key.page_down, 51 | '': Key.page_up, 52 | '': Key.right, 53 | '': Key.shift, 54 | '': Key.shift_l, 55 | '': Key.shift_r, 56 | '': Key.space, 57 | '': Key.tab, 58 | '': Key.up, 59 | '': Key.media_play_pause, 60 | '': Key.media_volume_mute, 61 | '': Key.media_volume_down, 62 | '': Key.media_volume_up, 63 | '': Key.media_previous, 64 | '': Key.media_next, 65 | '': Key.insert, 66 | '': Key.menu, 67 | '': Key.num_lock, 68 | '': Key.pause, 69 | '': Key.print_screen, 70 | '': Key.scroll_lock 71 | } 72 | key_dict[''].vk = 164 73 | key_dict[''].vk = 164 74 | key_dict[''].vk = 165 75 | key_dict[''].vk = 165 76 | key_dict[''].vk = 8 77 | key_dict[''].vk = 20 78 | key_dict[''].vk = 91 79 | key_dict[''].vk = 91 80 | key_dict[''].vk = 92 81 | key_dict[''] = key_dict[''] 82 | key_dict[''] = key_dict[''] 83 | key_dict[''] = key_dict[''] 84 | key_dict[''].vk = 162 85 | key_dict[''].vk = 162 86 | key_dict[''].vk = 163 87 | key_dict[''].vk = 46 88 | key_dict[''].vk = 40 89 | key_dict[''].vk = 35 90 | key_dict[''].vk = 13 91 | key_dict[''].vk = 27 92 | key_dict[''].vk = 112 93 | key_dict[''].vk = 113 94 | key_dict[''].vk = 114 95 | key_dict[''].vk = 115 96 | key_dict[''].vk = 116 97 | key_dict[''].vk = 117 98 | key_dict[''].vk = 118 99 | key_dict[''].vk = 119 100 | key_dict[''].vk = 120 101 | key_dict[''].vk = 121 102 | key_dict[''].vk = 122 103 | key_dict[''].vk = 123 104 | key_dict[''].vk = 124 105 | key_dict[''].vk = 125 106 | key_dict[''].vk = 126 107 | key_dict[''].vk = 127 108 | key_dict[''].vk = 128 109 | key_dict[''].vk = 129 110 | key_dict[''].vk = 130 111 | key_dict[''].vk = 131 112 | key_dict[''].vk = 36 113 | key_dict[''].vk = 37 114 | key_dict[''].vk = 34 115 | key_dict[''].vk = 33 116 | key_dict[''].vk = 39 117 | key_dict[''].vk = 160 118 | key_dict[''].vk = 160 119 | key_dict[''].vk = 161 120 | key_dict[''].vk = 32 121 | key_dict[''].vk = 9 122 | key_dict[''].vk = 38 123 | key_dict[''].vk = 179 124 | key_dict[''].vk = 173 125 | key_dict[''].vk = 174 126 | key_dict[''].vk = 175 127 | key_dict[''].vk = 177 128 | key_dict[''].vk = 176 129 | key_dict[''].vk = 45 130 | key_dict[''].vk = 93 131 | key_dict[''].vk = 144 132 | key_dict[''].vk = 19 133 | key_dict[''].vk = 44 134 | key_dict[''].vk = 145 135 | 136 | 137 | # 遍历A~Z 138 | for i in range(65, 91): 139 | t = chr(i) 140 | key_dict[t] = KeyCode.from_vk(i) 141 | key_dict[f'<{t}>'] = KeyCode.from_vk(i) 142 | key_dict[t.upper()] = KeyCode.from_vk(i) 143 | key_dict[f'<{t.upper()}>'] = KeyCode.from_vk(i) 144 | # 小键盘 145 | key_dict[f''] = KeyCode.from_vk(106) 146 | key_dict[f''] = KeyCode.from_vk(106) 147 | key_dict[f''] = KeyCode.from_vk(107) 148 | key_dict[f''] = KeyCode.from_vk(107) 149 | key_dict[f''] = KeyCode.from_vk(108) 150 | key_dict[f''] = KeyCode.from_vk(109) 151 | key_dict[f''] = KeyCode.from_vk(109) 152 | key_dict[f''] = KeyCode.from_vk(110) 153 | key_dict[f''] = KeyCode.from_vk(110) 154 | key_dict[f''] = KeyCode.from_vk(111) 155 | key_dict[f''] = KeyCode.from_vk(111) 156 | # 符号 157 | key_dict['<`>'] = KeyCode.from_vk(192) 158 | key_dict['`'] = KeyCode.from_vk(192) 159 | key_dict['<~>'] = KeyCode.from_vk(192) 160 | key_dict['~'] = KeyCode.from_vk(192) 161 | key_dict[''] = KeyCode.from_vk(49) 162 | key_dict['!'] = KeyCode.from_vk(49) 163 | key_dict['<@>'] = KeyCode.from_vk(50) 164 | key_dict['@'] = KeyCode.from_vk(50) 165 | key_dict['<#>'] = KeyCode.from_vk(51) 166 | key_dict['#'] = KeyCode.from_vk(51) 167 | key_dict['<$>'] = KeyCode.from_vk(52) 168 | key_dict['$'] = KeyCode.from_vk(52) 169 | key_dict['<%>'] = KeyCode.from_vk(53) 170 | key_dict['%'] = KeyCode.from_vk(53) 171 | key_dict['<^>'] = KeyCode.from_vk(54) 172 | key_dict['^'] = KeyCode.from_vk(54) 173 | key_dict['<&>'] = KeyCode.from_vk(55) 174 | key_dict['&'] = KeyCode.from_vk(55) 175 | key_dict['<*>'] = KeyCode.from_vk(56) 176 | key_dict['*'] = KeyCode.from_vk(56) 177 | key_dict['<(>'] = KeyCode.from_vk(57) 178 | key_dict['('] = KeyCode.from_vk(57) 179 | key_dict['<)>'] = KeyCode.from_vk(48) 180 | key_dict[')'] = KeyCode.from_vk(48) 181 | key_dict['<_>'] = KeyCode.from_vk(189) 182 | key_dict['_'] = KeyCode.from_vk(189) 183 | key_dict['<->'] = KeyCode.from_vk(189) 184 | key_dict['-'] = KeyCode.from_vk(189) 185 | key_dict['<=>'] = KeyCode.from_vk(187) 186 | key_dict['='] = KeyCode.from_vk(187) 187 | key_dict['<+>'] = KeyCode.from_vk(187) 188 | key_dict['+'] = KeyCode.from_vk(187) 189 | key_dict['<|>'] = KeyCode.from_vk(220) 190 | key_dict['|'] = KeyCode.from_vk(220) 191 | key_dict['<\\>'] = KeyCode.from_vk(220) 192 | key_dict['\\'] = KeyCode.from_vk(220) 193 | key_dict['<;>'] = KeyCode.from_vk(186) 194 | key_dict[';'] = KeyCode.from_vk(186) 195 | key_dict['<:>'] = KeyCode.from_vk(186) 196 | key_dict[':'] = KeyCode.from_vk(186) 197 | key_dict["<[>"] = KeyCode.from_vk(219) 198 | key_dict["["] = KeyCode.from_vk(219) 199 | key_dict["<{>"] = KeyCode.from_vk(219) 200 | key_dict['{'] = KeyCode.from_vk(219) 201 | key_dict['<]>'] = KeyCode.from_vk(221) 202 | key_dict['<}>'] = KeyCode.from_vk(221) 203 | key_dict[']'] = KeyCode.from_vk(221) 204 | key_dict['}'] = KeyCode.from_vk(221) 205 | key_dict[''] = KeyCode.from_vk(191) 206 | key_dict['/'] = KeyCode.from_vk(191) 207 | key_dict[''] = KeyCode.from_vk(191) 208 | key_dict['?'] = KeyCode.from_vk(191) 209 | key_dict['<\'>'] = KeyCode.from_vk(222) 210 | key_dict['\''] = KeyCode.from_vk(222) 211 | key_dict['<">'] = KeyCode.from_vk(222) 212 | key_dict['"'] = KeyCode.from_vk(222) 213 | key_dict['<,>'] = KeyCode.from_vk(188) 214 | key_dict['<<>'] = KeyCode.from_vk(188) 215 | key_dict['<'] = KeyCode.from_vk(188) 216 | key_dict[','] = KeyCode.from_vk(188) 217 | key_dict['<.>'] = KeyCode.from_vk(190) 218 | key_dict['<>>'] = KeyCode.from_vk(190) 219 | key_dict['>'] = KeyCode.from_vk(190) 220 | key_dict['.'] = KeyCode.from_vk(190) 221 | # 0~9 222 | for i in range(48, 58): 223 | t = str(i - 48) 224 | key_dict[t] = KeyCode.from_vk(i) 225 | key_dict[f'<{t}>'] = KeyCode.from_vk(i) 226 | # 小键盘 0~9 227 | for i in range(96, 106): 228 | t = str(i - 96) 229 | key_dict[f'numpad_{t}'] = KeyCode.from_vk(i) 230 | key_dict[f'numpad{t}'] = KeyCode.from_vk(i) 231 | key_dict[f''] = KeyCode.from_vk(i) 232 | key_dict[f''] = KeyCode.from_vk(i) 233 | 234 | key_vk_dict = {v.vk: k for k, v in key_dict.items() if '+' not in k and '<' in k and '>' in k} 235 | 236 | def vk_to_key_str(vk_code : int) -> str: 237 | return key_vk_dict[vk_code] 238 | 239 | def vks_to_key_str(vk_codes : list|set) -> str: 240 | vk_codes = set(vk_codes) 241 | result = '+'.join([vk_to_key_str(vk_code) for vk_code in vk_codes]) 242 | return result 243 | 244 | class GlobalHotKeyManager: 245 | """一利用GlobalHotKeys注册全局快捷键,提供注册函数和删除函数""" 246 | 247 | def __init__(self): 248 | self.hotkeys = {} # 一个字典,存储已注册的全局快捷键和对应的回调函数 249 | self.kbl = None 250 | 251 | def register(self, keys: set | list, callback=None): 252 | """注册函数,将set包含的按键组合注册为全局快捷键,传入回调函数,当按键组合被触发时执行,如果不传入回调函数,使用默认的回调函数""" 253 | key_codes = [key for key in keys if key] 254 | self.hotkeys['+'.join([str(i) for i in key_codes])] = callback if not callback is None else lambda: print( 255 | f'{keys} is pressed') 256 | 257 | is_run_set = set() 258 | 259 | def _run(self, press_key_set): 260 | 261 | # 检查要运行哪些函数 262 | # 遍历hotkeys 263 | for key, call_back in self.hotkeys.items(): 264 | # 检查是否所有按键且不在is_run_set中 265 | key_flag = all([key_dict[i].vk in press_key_set for i in key.split('+') if i in key_dict]) if key else False 266 | if key_flag and not key in self.is_run_set: 267 | # 运行 268 | self.is_run_set.add(key) 269 | call_back() 270 | elif not key_flag: 271 | # 将key从is_run_set中移除 272 | if key in self.is_run_set: 273 | self.is_run_set.discard(key) 274 | 275 | def start(self): 276 | self.kbl = KeyboardListener(self._run) 277 | self.kbl.start() 278 | 279 | def stop(self): 280 | """删除函数,用于删除所有通过注册函数注册的全局快捷键""" 281 | try: 282 | self.kbl.stop() 283 | except AttributeError: 284 | pass 285 | self.kbl = None 286 | 287 | def auto_register(self, config, ocr_func=None, setting_func=None, other_func=None): 288 | if not ocr_func is None: 289 | self.ocr_func = ocr_func 290 | if not setting_func is None: 291 | self.setting_func = setting_func 292 | if not other_func is None: 293 | self.other_func = other_func 294 | # 清空字典 295 | self.hotkeys.clear() 296 | ocr_keys = config.get('OCRKEY','+<->|+<->').split('|') 297 | setting_keys = config.get('SETTINGKEY','+<=>|+<=>').split('|') 298 | for keys in ocr_keys: 299 | self.register(keys.split('+'), self.ocr_func) 300 | for keys in setting_keys: 301 | self.register(keys.split('+'), self.setting_func) 302 | for i in range(1, 11): 303 | for keys in config.get(f'SKEY{i}',f'+<{i}>|+<{i}>').split('|'): 304 | self.register(keys.split('+'), lambda x=i: self.other_func(x)) 305 | for keys in config.get(f'SKEYANDOCR{i}',f'').split('|'): 306 | self.register(keys.split('+'), lambda x=i: self.other_func(x, True)) 307 | 308 | 309 | class KeyboardListener: 310 | def __init__(self, function = lambda x:None, scanningFrequency=128, on_press_function = lambda x:None, on_release_function = lambda x:None): 311 | self.function = function # 要执行的函数 312 | self.scanningFrequency = scanningFrequency # 上报频率 313 | self.press_key_set = set() 314 | self.stop_event = threading.Event() 315 | self.listener_thread = None 316 | self.scanner_thread = None 317 | self.on_press_function = on_press_function 318 | self.on_release_function = on_release_function 319 | 320 | @run_in_thread 321 | def on_press(self, key, _): 322 | # 记录按下的键 323 | self.press_key_set.add(key.vk) 324 | self.on_press_function(t) 325 | 326 | @run_in_thread 327 | def on_release(self, key, _): 328 | # 移除松开的键 329 | t = key.vk 330 | try: 331 | self.press_key_set.remove(t) 332 | except KeyError: 333 | # 清除set 334 | logging.warning("发生错误,清空按键") 335 | self.press_key_set.clear() 336 | self.on_release_function(t) 337 | 338 | def start_listener(self): 339 | # 使用with语句创建一个键盘监听器,监听键盘按键按下和释放事件 340 | with keyboard.Listener( 341 | on_press=self.on_press, 342 | on_release=self.on_release) as listener: 343 | while not self.stop_event.is_set(): 344 | time.sleep(0.01) # 检查停止事件 345 | 346 | old_press_key_set = set() 347 | def start_scanner(self): 348 | while not self.stop_event.is_set(): 349 | time.sleep(1 / float(self.scanningFrequency)) 350 | if self.press_key_set != self.old_press_key_set: 351 | self.old_press_key_set = self.press_key_set.copy() 352 | self.function(self.press_key_set) 353 | 354 | def start(self): 355 | self.listener_thread = threading.Thread(target=self.start_listener) 356 | self.scanner_thread = threading.Thread(target=self.start_scanner) 357 | 358 | self.listener_thread.start() 359 | self.scanner_thread.start() 360 | 361 | def stop(self): 362 | if not self.listener_thread is None and not self.scanner_thread is None: 363 | # 设置停止事件,通知监听器和扫描器线程停止 364 | self.stop_event.set() 365 | # 等待线程实际退出 366 | self.listener_thread.join() 367 | self.scanner_thread.join() 368 | 369 | 370 | # 随机延迟 371 | def random_sleep(min: float = 0.05, max: float = 0.1) -> None: 372 | time.sleep(random.uniform(min, max)) 373 | 374 | keyboard_c = Controller() 375 | def press_and_release(key, config) -> None: 376 | """按下并释放一个键""" 377 | global keyboard_c 378 | delay_min, delay_max = float(config.get('DELAY_MIN',0.05)), float(config.get('DELAY_MAX',0.1)) 379 | keyboard_c.press(key) 380 | random_sleep(delay_min, delay_max) 381 | keyboard_c.release(key) 382 | random_sleep(delay_min, delay_max) 383 | 384 | def c(line_s : str, config = None, activation = False): 385 | global keyboard_c 386 | # 更新config 387 | if not config: 388 | config = getConfigDict() 389 | if activation: 390 | delay_min, delay_max = float(config.get('DELAY_MIN',0.05)), float(config.get('DELAY_MAX',0.1)) 391 | keyboard_c.press(key_dict[config.get("ACTIVATION",'')]) 392 | random_sleep(delay_min, delay_max) 393 | for s in line_s: 394 | if not s: 395 | continue 396 | match s: 397 | case 'W': 398 | press_and_release(key_dict[config.get("W",'')], config) 399 | case 'S': 400 | press_and_release(key_dict[config.get("S",'')], config) 401 | case 'A': 402 | press_and_release(key_dict[config.get("A",'')], config) 403 | case 'D': 404 | press_and_release(key_dict[config.get("D",'')], config) 405 | if activation: 406 | keyboard_c.release(key_dict[config.get("ACTIVATION",'')]) -------------------------------------------------------------------------------- /util/imageProcessing.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import io 3 | import json 4 | import logging 5 | import os 6 | import mss 7 | import mss.tools 8 | from PIL import Image 9 | from collections import deque 10 | try: 11 | from util.loadSetting import getConfigDict 12 | except ModuleNotFoundError: 13 | from loadSetting import getConfigDict 14 | 15 | 16 | def rotate_left_90(matrix): 17 | # 反转每一行 18 | reversed_rows = [row[::-1] for row in matrix] 19 | # 转置并转换为列表的列表 20 | rotated = [list(row) for row in zip(*reversed_rows)] 21 | return rotated 22 | 23 | 24 | # 遍历arrow下所有图片 25 | arrow_data = { 26 | 'W': [], 27 | 'A': [], 28 | 'S': [], 29 | 'D': [] 30 | } 31 | for filename in os.listdir('./arrow'): 32 | # 转换为二维数组放入w_arrow_list 33 | img = Image.open('./arrow/' + filename).convert('L') 34 | img_array = list(img.getdata()) 35 | img_array = [img_array[i:i+img.width] 36 | for i in range(0, len(img_array), img.width)] 37 | arrow_data['W'].append(img_array) 38 | img_array = rotate_left_90(img_array) 39 | arrow_data['A'].append(img_array) 40 | img_array = rotate_left_90(img_array) 41 | arrow_data['S'].append(img_array) 42 | img_array = rotate_left_90(img_array) 43 | arrow_data['D'].append(img_array) 44 | 45 | 46 | def determine_arrow_direction(image_path = '' , img = None): 47 | if not img: 48 | img = Image.open(image_path).convert('L') 49 | img_array = list(img.getdata()) 50 | img_array = [img_array[i:i+img.width] 51 | for i in range(0, len(img_array), img.width)] 52 | # 计算匹配程度(示例数组是0的地方变成1扣2分,1的地方变成0不扣分,和原本一样加一分) 53 | 54 | def sum_score(img_array, arrow): 55 | score = 0 56 | for i in range(len(img_array)): 57 | for j in range(len(img_array)): 58 | if img_array[i][j] == arrow[i][j]: 59 | score += 1 60 | elif img_array[i][j] == 1 and arrow[i][j] == 0: 61 | score -= 1 62 | return score 63 | max_score = -100000 64 | result = '' 65 | for tag, arrow_list in arrow_data.items(): 66 | for arrow in arrow_list: 67 | score = sum_score(img_array, arrow) 68 | if score > max_score: 69 | max_score = score 70 | result = tag 71 | if max_score < 150: 72 | result = '' 73 | return result 74 | 75 | 76 | def arrow_str(input_dir='./temp/split_images'): 77 | result = '' 78 | end_dirname = [i for i in os.listdir( 79 | input_dir) if os.path.isdir(os.path.join(input_dir, i))][-1] 80 | # 输入目录,遍历每一个子文件夹,跳过文件 81 | for dirname in os.listdir(input_dir): 82 | if not os.path.isdir(os.path.join(input_dir, dirname)): 83 | continue 84 | # 遍历子文件夹中每一个bmp文件 85 | for filename in sorted(os.listdir(os.path.join(input_dir, dirname)), key=lambda x: int(os.path.splitext(x)[0])): 86 | if not filename.lower().endswith('.bmp'): 87 | continue 88 | result += determine_arrow_direction( 89 | os.path.join(input_dir, dirname, filename)) 90 | # 如果不是最后一行则添加换行 91 | if dirname != end_dirname: 92 | result += '\n' 93 | # 移除最后一个换行 94 | return result 95 | 96 | def arrow_str_fast(imgss): 97 | result = '' 98 | for idx, imgs in enumerate(imgss): 99 | if imgs is None: 100 | result += '\n' 101 | continue 102 | for img in imgs: 103 | result += determine_arrow_direction(img=img) 104 | if idx != len(imgss) - 1: 105 | result += '\n' 106 | return result 107 | 108 | def process_images_core(img, target_size=(15, 15)): 109 | imgs = [] 110 | if img is None: 111 | return imgs 112 | width, height = img.size 113 | pixels = img.load() 114 | # 初始化访问矩阵和区域列表 115 | visited = [[False for _ in range(width)] for _ in range(height)] 116 | regions = [] 117 | # 判断白色像素 118 | def is_white(pixel): 119 | return pixel > 200 120 | # 遍历所有像素寻找连通区域 121 | for y in range(height): 122 | for x in range(width): 123 | if not visited[y][x] and is_white(pixels[x, y]): # type: ignore 124 | # BFS遍历连通区域 125 | queue = deque() 126 | queue.append((x, y)) 127 | visited[y][x] = True 128 | region_points = [] 129 | while queue: 130 | x0, y0 = queue.popleft() 131 | region_points.append((x0, y0)) 132 | # 检查8邻域 133 | for dx, dy in [(-1, -1), (-1, 0), (-1, 1), 134 | (0, -1), (0, 1), 135 | (1, -1), (1, 0), (1, 1)]: 136 | nx, ny = x0 + dx, y0 + dy 137 | if 0 <= nx < width and 0 <= ny < height and not visited[ny][nx] and is_white(pixels[nx, ny]): # type: ignore 138 | visited[ny][nx] = True 139 | queue.append((nx, ny)) 140 | # 过滤小区域 141 | if len(region_points) >= 25: 142 | # 计算包围盒 143 | min_x = min(x for x, y in region_points) 144 | max_x = max(x for x, y in region_points) 145 | min_y = min(y for x, y in region_points) 146 | max_y = max(y for x, y in region_points) 147 | regions.append((min_x, min_y, max_x, max_y)) 148 | # 按从左到右、从上到下排序 149 | regions.sort(key=lambda r: (r[0], r[1])) 150 | # 处理每个有效区域 151 | for index, (x1, y1, x2, y2) in enumerate(regions): 152 | # 裁剪并缩放 153 | region_img = img.crop((x1, y1, x2 + 1, y2 + 1)) 154 | resized_img = region_img.resize( 155 | target_size, Image.Resampling.NEAREST) 156 | imgs.append(resized_img) 157 | return imgs 158 | 159 | def process_images(directory='./temp/split_images', target_size=(15, 15), fast_mode = False, imgs:list = []): 160 | """ 161 | 处理指定目录下的所有n.bmp图片,切割连续白色区域并保存到对应编号的文件夹 162 | 163 | 参数: 164 | input_dir - 输入目录路径(包含0.bmp, 1.bmp等数字命名的图片) 165 | target_size - 缩放目标尺寸,默认6x6像素 166 | """ 167 | if not fast_mode: 168 | for filename in os.listdir(directory): 169 | if not filename.endswith('.bmp'): 170 | continue 171 | # 创建输出文件夹 172 | base_name = filename.split('.')[0] 173 | output_dir = os.path.join(directory, base_name) 174 | os.makedirs(output_dir, exist_ok=True) 175 | # 打开并转换图像 176 | img = Image.open(os.path.join(directory, filename)).convert('L') 177 | # 处理图像并保存结果 178 | imgs = process_images_core(img, target_size) 179 | for index, resized_img in enumerate(imgs): 180 | # 保存结果 181 | resized_img.save(os.path.join(output_dir, f"{index}.bmp")) 182 | else: 183 | imgss = [] 184 | for img in imgs: 185 | imgss.append(process_images_core(img, target_size)) 186 | return imgss 187 | 188 | 189 | def split_image(image_path='./temp/screenshot_binary.bmp', save_dir='./temp/split_images', fast_mode = False, img = None): 190 | imgs = [] 191 | # 打开图片并转换为灰度图 192 | if not fast_mode: 193 | img = Image.open(image_path).convert('L') 194 | width, height = img.size 195 | 196 | # 寻找第一列满足连续15个白像素的列 197 | target_col = -1 198 | for col in range(width): 199 | consecutive = 0 200 | for row in range(height): 201 | if img.getpixel((col, row)) == 255: 202 | consecutive += 1 203 | if consecutive >= 15: 204 | target_col = col 205 | break 206 | else: 207 | consecutive = 0 208 | if target_col != -1: 209 | break 210 | 211 | if target_col == -1: 212 | logging.warning("未找到符合条件的列") 213 | return 214 | 215 | # 在目标列及其右侧两列中查找所有有效竖列 216 | columns_to_check = [c for c in range(target_col, min(target_col+3, width))] 217 | segments = [] 218 | for col in columns_to_check: 219 | current_segments = [] 220 | start = -1 221 | for row in range(height): 222 | pixel = img.getpixel((col, row)) 223 | if pixel == 255: 224 | if start == -1: 225 | start = row 226 | # 处理行结束或遇到黑像素的情况 227 | if row == height-1 or img.getpixel((col, row+1)) != 255: 228 | if row - start + 1 >= 15: 229 | current_segments.append((col, start, row)) 230 | start = -1 231 | else: 232 | if start != -1: 233 | if (row-1) - start + 1 >= 15: 234 | current_segments.append((col, start, row-1)) 235 | start = -1 236 | segments.extend(current_segments) 237 | 238 | # 按起始行排序并过滤高度相近的竖列(保留最左侧) 239 | sorted_segments = sorted(segments, key=lambda x: x[1]) 240 | filtered_segments = [] 241 | current_group = [] 242 | for seg in sorted_segments: 243 | if not current_group: 244 | current_group.append(seg) 245 | else: 246 | if seg[1] - current_group[0][1] < 5: 247 | current_group.append(seg) 248 | else: 249 | # 选择当前组中最左侧的竖列 250 | filtered_segments.append( 251 | min(current_group, key=lambda x: x[0])) 252 | current_group = [seg] 253 | if current_group: 254 | filtered_segments.append(min(current_group, key=lambda x: x[0])) 255 | 256 | # 小列补全 257 | # 计算小列平均高度 258 | avg_height = sum([seg[2] - seg[1] for seg in filtered_segments]) / len(filtered_segments) 259 | new_filtered_segments = [] 260 | spacings = [] 261 | idx_temp = 0 262 | # 记录补全的idx 263 | idx_ext_list = [] 264 | # 从第二个开始遍历小列,如果和上一个小列的间距大于小列平均高度,则在中间插入一个小列 265 | for idx, (col, s_row, e_row) in enumerate(filtered_segments): 266 | # 跳过第一个 267 | if idx == 0: 268 | new_filtered_segments.append((col, s_row, e_row)) 269 | continue 270 | prev_col, prev_s_row, prev_e_row = filtered_segments[idx - 1] 271 | spacing = s_row - prev_e_row 272 | if spacing > avg_height: 273 | # 根据spacings计算平均间隔,如果不存在默认为9 274 | avg_spacing = sum(spacings) / len(spacings) if spacings else 9 275 | # 计算能插几个小列(小列高度+平均间隔) 276 | num_inserted = int(spacing / (avg_height + avg_spacing)) 277 | # 插入小列 278 | for i in range(num_inserted): 279 | idx_temp += 1 280 | idx_ext_list.append(idx_temp) 281 | # 取整 282 | s_new = int(prev_e_row + ((i + 1) * avg_spacing) + (i * avg_height)) 283 | e_new = s_new + int(avg_height) - 1 284 | new_filtered_segments.append((col, s_new, e_new)) 285 | else: 286 | idx_temp += 1 287 | spacings.append(spacing) 288 | new_filtered_segments.append((col, s_row, e_row)) 289 | filtered_segments = new_filtered_segments 290 | 291 | # 创建保存目录 292 | if not fast_mode: 293 | os.makedirs(save_dir, exist_ok=True) 294 | 295 | found_col_end_list = [] 296 | screenshot_icon_point_list = [] 297 | # 处理每个有效竖列 298 | for idx, (col, s_row, e_row) in enumerate(filtered_segments): 299 | found_row = -1 300 | found_col_end = -1 301 | imgs.append(None) 302 | 303 | # 如果不在idx_ext_list中,在竖列高度范围内寻找有效行 304 | if not idx in idx_ext_list: 305 | for row in range(s_row -1, e_row + 2): 306 | consecutive = 0 307 | # 从当前竖列位置向右扫描 308 | for c in range(col + 1, width): 309 | if img.getpixel((c, row)) == 255: 310 | consecutive += 1 311 | if consecutive >= 15: 312 | found_col_end = c 313 | else: 314 | break # 遇到黑像素停止 315 | # 确认找到足够长度的连续白像素 316 | if consecutive >= 15: 317 | found_row = row 318 | break 319 | if found_col_end != -1: 320 | found_col_end_list.append(found_col_end) 321 | else: 322 | # 如果是补全的小列 323 | # 第一行作为小行,用之前小行的平均宽度 324 | found_col_end = int(sum(found_col_end_list) / len(found_col_end_list)) if found_col_end_list else 32 325 | found_row = s_row 326 | 327 | if found_row != -1: 328 | # 计算裁剪区域(PIL坐标系) 329 | left = found_col_end + 3 + 8 # 小列右侧偏移量 330 | upper_mid = (found_row + ((e_row - found_row) / 2) 331 | if (e_row - found_row) > 15 332 | else s_row + ((e_row - s_row) / 2)) 333 | upper = int(upper_mid) # 取下半区域 334 | right = width # 右边界exclusive 335 | lower = e_row + 1 336 | 337 | # 执行裁剪并保存 338 | cropped_img = img.crop((left, upper, right, lower)) 339 | if fast_mode: 340 | imgs[idx] = cropped_img 341 | else: 342 | cropped_img.save(os.path.join(save_dir, f'{idx}.bmp')) 343 | # 记录小列对应图标区域坐标(小列是图标方形区域左边) 344 | screenshot_icon_point_list.append({ 345 | 'x1' : col, 346 | 'y1' : s_row, 347 | 'x2' : col + found_col_end, 348 | 'y2' : e_row 349 | }) 350 | if not fast_mode: 351 | # 将小列对应图标区域坐标保存到./temp/screenshot_icon_point.json 352 | ''' 353 | ```json 354 | [ 355 | { 356 | 'x1' : 左上坐标x, 357 | 'y1' : 左上坐标y, 358 | 'x2' : 右下坐标x, 359 | 'y2' : 右下坐标y 360 | } 361 | ] 362 | ``` 363 | ''' 364 | # 保存 365 | with open('./temp/screenshot_icon_point.json', 'w', encoding='utf-8') as f: 366 | f.write(json.dumps(screenshot_icon_point_list, indent=4, ensure_ascii=False)) 367 | 368 | return imgs 369 | 370 | def read_bmp_to_png_base64(image_path): 371 | try: 372 | with open(image_path, 'rb') as f: 373 | bmp_data = f.read() 374 | # 将字节数据转换为Base64编码并解码为字符串 375 | return 'data:image/png;base64,' + base64.b64encode(bmp_data).decode("utf-8") 376 | except Exception as e: 377 | print(f"Error reading BMP file: {e}") 378 | return '' 379 | 380 | def image_to_png_base64(image): 381 | # 创建内存字节缓冲 382 | with io.BytesIO() as buffer: 383 | # 将图片保存为PNG格式到缓冲区 384 | image.save(buffer, format="PNG") 385 | # 获取字节数据 386 | png_bytes = buffer.getvalue() 387 | # 转换为Base64编码并解码为字符串 388 | return base64.b64encode(png_bytes).decode("utf-8") 389 | 390 | def screenshot_icon_crop_to_base64(point_dict : dict): 391 | ''' 392 | :param point_dict: {'x1' : 左上坐标x,'y1' : 左上坐标y,'x2' : 右下坐标x,'y2' : 右下坐标y} 393 | :return: base64编码的图标 394 | ''' 395 | config = getConfigDict() 396 | # 计算裁剪前的坐标 397 | ''' 398 | LEFT=30 399 | TOP=20 400 | RIGHT=220 401 | BOTTOM=400 402 | ''' 403 | # 计算缩放后裁剪前的坐标 404 | x1 = int(point_dict['x1'] + float(config['LEFT'])) 405 | y1 = int(point_dict['y1'] + float(config['TOP'])) 406 | x2 = int(point_dict['x2'] + float(config['LEFT'])) 407 | y2 = int(point_dict['y2'] + float(config['TOP'])) 408 | # 读取截图 409 | screenshot = Image.open('./temp/screenshot.png') 410 | # 计算缩放前的坐标(缩放后会等比缩放,将高缩放到720) 411 | # 读取缩放前的宽高 412 | original_width, original_height = screenshot.size 413 | # 计算缩放比例 414 | scale = original_height / 720 415 | # 计算缩放前的坐标 416 | x1 = int(x1 * scale) 417 | y1 = int(y1 * scale) 418 | x2 = int(x2 * scale) 419 | y2 = int(y2 * scale) 420 | # 裁剪 421 | icon_img = screenshot.crop((x1, y1, x2, y2)) 422 | # 添加Base64数据URI的前缀 423 | icon_img_base64 = 'data:image/png;base64,' + image_to_png_base64(icon_img) 424 | return icon_img_base64 425 | 426 | def hex_to_rgb(hex_color): 427 | """ 428 | 将16位颜色代码转换为RGB格式 429 | :param hex_color: 16位颜色代码,例如'#FF5733' 430 | :return: RGB元组,例如(255, 87, 51) 431 | """ 432 | hex_color = hex_color.lstrip('#') 433 | return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) 434 | 435 | 436 | def color_to_grayscale(color, threshold=None, target_colors = [hex_to_rgb(c) for c in '#DAC177,#DF7567,#50AFC8,#74A15F,#BEBEBE,#BAB9A1,#E4D0AA'.split(',')]): 437 | """ 438 | 将颜色转换为灰度值,如果颜色在指定范围内,则返回255(白色),否则返回0(黑色)。 439 | 440 | 参数: 441 | color (tuple): RGB颜色值。 442 | threshold (int): 允许的颜色范围。 443 | """ 444 | 445 | for target in target_colors: 446 | if all(abs(c1 - c2) <= threshold for c1, c2 in zip(color, target)): 447 | return 255 # 白色 448 | return 0 # 黑色 449 | 450 | def binarize_image_core(threshold, colors, img): 451 | """ 452 | 将图片按规则二值化。 453 | 454 | 参数: 455 | threshold (int): 颜色匹配的阈值。 456 | colors (list): 目标颜色列表。 457 | img (PIL.Image): PIL图片对象。 458 | """ 459 | # 将图片转换为RGB模式 460 | img_rgb = img.convert('RGB') 461 | # 创建一个新的图片用于存储二值化结果 462 | img_binary = Image.new('L', img_rgb.size) 463 | # 遍历每个像素 464 | for x in range(img_rgb.width): 465 | for y in range(img_rgb.height): 466 | # 获取当前像素的颜色 467 | r, g, b = img_rgb.getpixel((x, y)) 468 | # 将颜色转换为灰度值 469 | gray_value = color_to_grayscale((r, g, b), threshold, colors) 470 | # 设置新的像素值 471 | img_binary.putpixel((x, y), gray_value) 472 | return img_binary 473 | 474 | def binarize_image(input_path='./temp/screenshot_resized.png', output_path='./temp/screenshot_binary.bmp', threshold=None, config = None, fast_mode = False, img = None): 475 | """ 476 | 将PNG图片按规则二值化并保存为BMP格式。 477 | 478 | 参数: 479 | input_path (str): 读取PNG图片的路径。 480 | output_path (str): 保存BMP图片的路径。 481 | threshold (int): 颜色匹配的阈值。 482 | """ 483 | if config is None: 484 | config = getConfigDict() 485 | colors = [hex_to_rgb(c) for c in config['COLORS'].split(',')] 486 | if threshold is None: 487 | threshold = int(config['THRESHOLD']) 488 | if fast_mode: 489 | return binarize_image_core(threshold, colors, img) 490 | try: 491 | # 打开图片 492 | with Image.open(input_path) as img: 493 | # 将图片转换为RGB模式 494 | img_rgb = img.convert('RGB') 495 | 496 | # 创建一个新的图片用于存储二值化结果 497 | img_binary = binarize_image_core(threshold, colors, img_rgb) 498 | 499 | # 保存图片 500 | img_binary.save(output_path, 'BMP') 501 | 502 | logging.debug(f"图片已成功二值化并保存到 {output_path}") 503 | except Exception as e: 504 | logging.warning(f"处理图片时发生错误: {e}") 505 | 506 | 507 | def crop_image(input_path='./temp/screenshot.png', output_path='./temp/screenshot_cropped.png', left=None, top=None, right=None, bottom=None, config = None,fast_mode = False, img = None): 508 | """ 509 | 截取PNG图片的左上角区域并保存。 510 | 511 | 参数: 512 | input_path (str): 读取PNG图片的路径。 513 | output_path (str): 保存PNG图片的路径。 514 | left (int): 截取区域的左边界。 515 | top (int): 截取区域的上边界。 516 | right (int): 截取区域的右边界。 517 | bottom (int): 截取区域的下边界。 518 | """ 519 | if config is None: 520 | config = getConfigDict() 521 | if left is None: 522 | left = int(float(config['LEFT'])) 523 | if top is None: 524 | top = int(float(config['TOP'])) 525 | if right is None: 526 | right = int(float(config['RIGHT'])) 527 | if bottom is None: 528 | bottom = int(float(config['BOTTOM'])) 529 | if fast_mode: 530 | return img.crop((left, top, right, bottom)) 531 | # 打开图片 532 | if input_path == output_path: 533 | output_path += '.crop_tmp' 534 | with Image.open(input_path) as img: 535 | # 截取图片 536 | cropped_img = img.crop((left, top, right, bottom)) 537 | # 保存图片 538 | cropped_img.save(output_path, 'PNG') 539 | logging.debug(f"图片已成功截取并保存到 {output_path}") 540 | # 如果末尾是'.crop_tmp',则删除原文件 541 | if output_path.endswith('.crop_tmp'): 542 | os.remove(input_path) 543 | os.rename(output_path, input_path) 544 | 545 | def resize_image_core(img): 546 | # 计算新的尺寸,保持宽高比 547 | original_width, original_height = img.size 548 | aspect_ratio = original_height / original_width 549 | new_width = 190 550 | new_height = int(new_width * aspect_ratio) 551 | # 调整图片尺寸 552 | try: 553 | resized_img = img.resize((new_width, new_height), Image.LANCZOS) 554 | except: 555 | resized_img = img.resize((new_width, new_height), Image.ANTIALIAS) 556 | return resized_img 557 | 558 | def resize_image(input_path='./temp/screenshot_cropped.png', output_path='./temp/screenshot_resized.png'): 559 | """ 560 | 等比例地将PNG图片的高度缩放到720像素并保存。 561 | 562 | 参数: 563 | input_path (str): 读取PNG图片的路径。 564 | output_path (str): 保存PNG图片的路径。 565 | """ 566 | # 打开图片 567 | with Image.open(input_path) as img: 568 | resized_img = resize_image_core(img) 569 | # 保存图片 570 | resized_img.save(output_path, 'PNG') 571 | logging.debug(f"图片已成功缩放并保存到 {output_path}") 572 | 573 | 574 | def capture_screenshot(save_path='./temp/screenshot.png',fast_mode=False): 575 | """ 576 | 获取屏幕截图并保存为PNG格式到指定路径 577 | 578 | :param save_path: 保存截图的路径 579 | """ 580 | # 如果save_path包含'temp/'且fast_mode为False且temp目录不存在,则创建temp目录 581 | if not fast_mode and 'temp/' in save_path and not os.path.exists('./temp'): 582 | os.makedirs('./temp') 583 | with mss.mss() as sct: 584 | # 获取屏幕的尺寸 585 | monitor = sct.monitors[0] 586 | 587 | # 获取屏幕截图 588 | screenshot = sct.grab(monitor) 589 | 590 | if fast_mode: 591 | image = Image.frombytes("RGB", screenshot.size, screenshot.rgb) 592 | return image 593 | 594 | # 保存截图为PNG格式 595 | mss.tools.to_png(screenshot.rgb, screenshot.size, output=save_path) 596 | 597 | def fast_arrow(config, img = None): 598 | if img is None: 599 | img = capture_screenshot(fast_mode=True) 600 | img = crop_image(img=img, fast_mode=True, config=config) 601 | img = resize_image_core(img) 602 | img = binarize_image(img=img, fast_mode=True, config=config) 603 | imgs = split_image(img=img, fast_mode=True) 604 | imgss = process_images(fast_mode=True, imgs=imgs) 605 | return arrow_str_fast(imgss) 606 | 607 | if __name__ == "__main__": 608 | import time 609 | # 记录开始时间 610 | # time.sleep(5) 611 | # start_time = time.time() 612 | # print(fast_arrow(getConfigDict(),Image.open('./temp/screenshot.png'))) 613 | # print(f'耗时: {time.time() - start_time} 秒') 614 | start_time = time.time() 615 | # capture_screenshot() 616 | crop_image() 617 | resize_image() 618 | 619 | binarize_image() 620 | split_image() 621 | process_images() 622 | print(arrow_str()) 623 | print(f'耗时: {time.time() - start_time} 秒') 624 | pass 625 | -------------------------------------------------------------------------------- /util/settingGUI.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import re 5 | 6 | from util.globalHotKeyManager import GlobalHotKeyManager, KeyboardListener, vk_to_key_str 7 | from util.loadSetting import getConfigFilePath, saveConfigDict, getConfigDict, getDefaultConfigDict 8 | from util.imageProcessing import capture_screenshot, crop_image, resize_image 9 | 10 | from PyQt6.QtWidgets import QApplication, QWidget, QDoubleSpinBox, QLabel, QPushButton, QTextEdit, QMessageBox, QCheckBox, QDialog, QVBoxLayout, QLineEdit 11 | from PyQt6.QtGui import QKeyEvent, QColor, QPainter, QBrush, QPen, QDesktopServices, QIcon, QGuiApplication 12 | from PyQt6.QtCore import Qt, QPoint, QUrl, QTimer, QObject, pyqtSignal 13 | 14 | keys = set() 15 | update_flag = False 16 | keys_record_flag = False 17 | def keyCallBack(callbak_keys): 18 | global keys,update_flag,keys_record_flag 19 | if not keys_record_flag: 20 | return 21 | if len(keys) half_w and pos.y() > half_h: 273 | self.resize_corner = True 274 | else: 275 | self.window().windowHandle().startSystemMove() 276 | 277 | self.drag_position = event.globalPosition().toPoint() 278 | 279 | def mouseMoveEvent(self, event): 280 | 281 | # cursor shape change 282 | pos = event.position().toPoint() 283 | half_w = self.width() // 2 284 | half_h = self.height() // 2 285 | 286 | shouldSetCursor = True 287 | # cursor not on resize place 288 | if pos.x() <= half_w or pos.y() <= half_h: 289 | shouldSetCursor = False 290 | # cursor on the save button 291 | if self.save_button.geometry().contains(pos): 292 | shouldSetCursor = False 293 | # should always keep resize cursor shape when resizing 294 | if self.resizing: 295 | shouldSetCursor = True 296 | 297 | if shouldSetCursor: 298 | self.setCursor(Qt.CursorShape.SizeFDiagCursor) 299 | else: 300 | self.setCursor(Qt.CursorShape.ArrowCursor) 301 | 302 | # window resize 303 | if not self.resizing or not self.resize_corner: 304 | return 305 | 306 | current_pos = event.globalPosition().toPoint() 307 | delta = current_pos - self.drag_position 308 | 309 | new_w = self.width() + delta.x() 310 | new_h = self.height() + delta.y() 311 | 312 | self.resize(max(self.MIN_W, new_w), max(self.MIN_H, new_h)) 313 | self.drag_position = current_pos 314 | 315 | def mouseReleaseEvent(self, event): 316 | self.resizing = False 317 | self.resize_corner = None 318 | 319 | 320 | class ipInputer(QLineEdit): 321 | def __init__(self, parent, defalutText): 322 | super().__init__(parent) 323 | 324 | self.setPlaceholderText("Host [ipv4 / ipv6]") 325 | self.setText(defalutText) 326 | 327 | self.textChanged.connect(self.onTextChanged) 328 | 329 | def onTextChanged(self): 330 | if self.isValidIpAddress(self.text()): 331 | self.setStyleSheet("background-color: white;") 332 | else: 333 | self.setStyleSheet("background-color: #ffaaaa;") 334 | 335 | def isValidIpAddress(self, text): 336 | ipv4 = r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' 337 | ipv6 = r'^([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4})$|^([0-9a-fA-F]{1,4}:){1,7}:$|^::([0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:){1,6}:([0-9a-fA-F]{1,4})$|^([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}$|^([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}$|^([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}$|^([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}$|^([0-9a-fA-F]{1,4}:){1}(:[0-9a-fA-F]{1,4}){1,6}$|^:((:[0-9a-fA-F]{1,4}){1,7}|:)$' 338 | 339 | pattern = re.compile(rf'^{ipv4}|{ipv6}|all$') 340 | return pattern.fullmatch(text) 341 | 342 | class portInputer(QLineEdit): 343 | def __init__(self, parent, defalutText): 344 | super().__init__(parent) 345 | 346 | self.setPlaceholderText("Port") 347 | self.setText(defalutText) 348 | 349 | self.textChanged.connect(self.onTextChanged) 350 | 351 | def onTextChanged(self): 352 | if self.isValidPort(self.text()): 353 | self.setStyleSheet("background-color: white;") 354 | else: 355 | self.setStyleSheet("background-color: #ffaaaa;") 356 | 357 | def isValidPort(self, text): 358 | port = r'^(0|[1-9][0-9]{0,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|65535)$' 359 | 360 | pattern = re.compile(port) 361 | return pattern.fullmatch(text) 362 | 363 | 364 | class settingPanel(QWidget): 365 | 366 | def __init__(self, qApp, config: dict, hotkeyManager: GlobalHotKeyManager): 367 | super().__init__() 368 | 369 | self.qApp = qApp 370 | self.config = config 371 | self.hotkeyMgr = hotkeyManager 372 | self.keybinds = {} 373 | self.is_closed = True 374 | 375 | self.setWindowTitle("HD2AS 设置面板") 376 | self.setWindowIcon(QIcon("./icon.png")) 377 | self.setFixedSize(200, 410) 378 | 379 | self.initWidgets() 380 | 381 | def initWidgets(self): 382 | 383 | # reset button # 384 | 385 | self.reset_button = QPushButton("重置所有设置", self) 386 | self.reset_button.setGeometry(10, 370, 85, 30) 387 | self.reset_button.setStyleSheet("color: red;") 388 | 389 | self.reset_button.clicked.connect(self.onResetButtonCliecked) 390 | 391 | # reset button end # 392 | 393 | # save button # 394 | 395 | self.save_button = QPushButton("保存所有设置", self) 396 | self.save_button.setGeometry(105, 370, 85, 30) 397 | 398 | self.save_button.clicked.connect(self.onSaveButtonCliecked) 399 | 400 | # save button end # 401 | 402 | # start with program # 403 | 404 | self.start_with_program_checkbox = QCheckBox("允许设置面板随程序开启", self) 405 | self.start_with_program_checkbox.setGeometry(10, 310, 180, 20) 406 | self.start_with_program_checkbox.setChecked(self.config.get("START_GUI_WITH_PROGRAM", "True").upper() == "TRUE") 407 | # this checkbox state will be saved when save button cliecked 408 | 409 | # start with program end # 410 | 411 | # manual edit button # 412 | 413 | self.manual_edit_button = QPushButton("(高级)手动编辑配置文件", self) 414 | self.manual_edit_button.setGeometry(10, 335, 180, 30) 415 | 416 | self.manual_edit_button.clicked.connect(self.onManualEditButtonCliecked) 417 | 418 | # manual edit button end # 419 | 420 | # spinbox # 421 | 422 | def createSpinbox_delay(label, h): 423 | delay_label = QLabel(label, self) 424 | delay_label.setGeometry(10, h, 120, 30) 425 | 426 | delay_spinbox = QDoubleSpinBox(self) 427 | delay_spinbox.setGeometry(130, h, 60, 30) 428 | 429 | delay_spinbox.setRange(0.0, 100.0) 430 | delay_spinbox.setDecimals(3) 431 | delay_spinbox.setSingleStep(0.001) 432 | 433 | return delay_spinbox 434 | 435 | # DELAY_MIN # 436 | 437 | self.delay_min_spinbox = createSpinbox_delay("按键随机最小延迟(s):", 10) 438 | self.delay_min_spinbox.setValue(float(self.config.get("DELAY_MIN", ""))) 439 | 440 | # DELAY_MAX # 441 | 442 | self.delay_max_spinbox = createSpinbox_delay("按键随机最大延迟(s):", 40) 443 | self.delay_max_spinbox.setValue(float(self.config.get("DELAY_MAX", ""))) 444 | 445 | # spinbox end # 446 | 447 | # keybind # 448 | 449 | self.keybind_button = QPushButton("配置键盘快捷键", self) 450 | self.keybind_button.setGeometry(10, 75, 180, 30) 451 | self.keybind_button.clicked.connect(self.onKeybindButtonCliecked) 452 | 453 | # keybind end # 454 | 455 | # resize panel # 456 | 457 | def createSpinbox_resizePanel(label, h, first): 458 | delay_label = QLabel(label, self) 459 | if first: 460 | delay_label.setGeometry(10, h, 20, 30) 461 | else: 462 | delay_label.setGeometry(105, h, 20, 30) 463 | 464 | delay_spinbox = QDoubleSpinBox(self) 465 | if first: 466 | delay_spinbox.setGeometry(35, h, 60, 30) 467 | else: 468 | delay_spinbox.setGeometry(130, h, 60, 30) 469 | 470 | delay_spinbox.setRange(0.0, 99999.0) 471 | delay_spinbox.setDecimals(0) 472 | delay_spinbox.setSingleStep(1) 473 | 474 | return delay_spinbox 475 | 476 | resize_label = QLabel("======== 识别区域设置 ========", self) 477 | resize_label.setGeometry(10, 110, 180, 25) 478 | resize_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 479 | 480 | 481 | self.size_x_spinbox = createSpinbox_resizePanel( "左:", 135, True ) 482 | self.size_x_spinbox.setValue(float(self.config.get("LEFT", ""))) 483 | 484 | self.size_y_spinbox = createSpinbox_resizePanel( "上:", 165, True ) 485 | self.size_y_spinbox.setValue(float(self.config.get("TOP", ""))) 486 | 487 | self.size_w_spinbox = createSpinbox_resizePanel( "右:", 135, False ) 488 | self.size_w_spinbox.setValue(float(self.config.get("RIGHT", ""))) 489 | 490 | self.size_h_spinbox = createSpinbox_resizePanel( "下:", 165, False ) 491 | self.size_h_spinbox.setValue(float(self.config.get("BOTTOM", ""))) 492 | 493 | self.resize_button = QPushButton("交互式更改", self) 494 | self.resize_button.setGeometry(10, 200, 85, 30) 495 | self.resize_button.clicked.connect(self.onResizeButtonCliecked) 496 | 497 | self.resize_test_button = QPushButton("截图测试", self) 498 | self.resize_test_button.setGeometry(105, 200, 85, 30) 499 | self.resize_test_button.clicked.connect(self.onResizeTestButtonCliecked) 500 | 501 | # resize panel end # 502 | 503 | # webui # 504 | 505 | webui_label = QLabel("======== WebUI 设置 ========", self) 506 | webui_label.setGeometry(10, 235, 180, 25) 507 | webui_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 508 | 509 | self.host_lineedit = ipInputer(self, self.config.get("WEB_GUI_HOST", "")) 510 | self.host_lineedit.setGeometry(10, 260, 110, 30) 511 | 512 | colon_label = QLabel(":", self) 513 | colon_label.setGeometry(120, 260, 10, 30) 514 | colon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 515 | 516 | self.port_lineedit = portInputer(self, str(self.config.get("WEB_GUI_PORT", ""))) 517 | self.port_lineedit.setGeometry(130, 260, 60, 30) 518 | 519 | 520 | # webui end # 521 | 522 | widgets = [ 523 | self.delay_min_spinbox, 524 | self.delay_max_spinbox, 525 | self.keybind_button, 526 | self.size_x_spinbox, 527 | self.size_w_spinbox, 528 | self.size_y_spinbox, 529 | self.size_h_spinbox, 530 | self.resize_button, 531 | self.resize_test_button, 532 | self.start_with_program_checkbox, 533 | self.manual_edit_button, 534 | self.reset_button, 535 | self.save_button 536 | ] 537 | for i in range(len(widgets) - 1): 538 | self.setTabOrder(widgets[i], widgets[i + 1]) 539 | 540 | # reset button # 541 | def onResetButtonCliecked(self): 542 | message_box = QMessageBox(self) 543 | message_box.setWindowTitle("警告") 544 | message_box.setText("你正在执行的操作:重置所有设置
警告:此操作不可逆") 545 | message_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) 546 | message_box.setDefaultButton(QMessageBox.StandardButton.No) 547 | message_box.button(QMessageBox.StandardButton.Yes).setText("确认") 548 | message_box.button(QMessageBox.StandardButton.No).setText("取消") 549 | message_box.setIcon(QMessageBox.Icon.Warning) 550 | 551 | reply = message_box.exec() 552 | if reply == QMessageBox.StandardButton.No: 553 | return 554 | 555 | 556 | saveConfigDict(getDefaultConfigDict()) 557 | self.config = getConfigDict() 558 | 559 | # delete all widgets and reinit them, so i dont need to change the value one by one 560 | self.hide() 561 | 562 | for child in self.findChildren(QWidget): 563 | child.deleteLater() 564 | self.initWidgets() 565 | 566 | self.show() 567 | # reset button end # 568 | 569 | # save button # 570 | def onSaveButtonCliecked(self): 571 | 572 | newConfig = dict(self.keybinds) 573 | 574 | try: 575 | newConfig .update({ 576 | "DELAY_MIN": self.delay_min_spinbox.value(), 577 | "DELAY_MAX": self.delay_max_spinbox.value(), 578 | #"ACTIVATION": self.keybind_label.toPlainText(), 579 | 580 | "LEFT": int(self.size_x_spinbox.value()), 581 | "TOP": int(self.size_y_spinbox.value()), 582 | "RIGHT": int(self.size_w_spinbox.value()), 583 | "BOTTOM": int(self.size_h_spinbox.value()), 584 | 585 | "START_GUI_WITH_PROGRAM": "True" if self.start_with_program_checkbox.isChecked() else "False", 586 | 587 | "WEB_GUI_PORT": int(self.port_lineedit.text()), 588 | "WEB_GUI_HOST": self.host_lineedit.text() 589 | }) 590 | except Exception as e: 591 | QMessageBox.critical(self, 'QMessageBox', '保存时发生错误!目前没有执行任何操作\n' + str(e), QMessageBox.StandardButton.Ok) 592 | return 593 | 594 | 595 | saveConfigDict(newConfig) 596 | self.config = getConfigDict() 597 | self.hotkeyMgr.auto_register(self.config) 598 | 599 | self.close() 600 | # save button end # 601 | 602 | # manual edit button # 603 | def onManualEditButtonCliecked(self): 604 | message_box = QMessageBox(self) 605 | message_box.setWindowTitle("警告") 606 | message_box.setText("这会放弃所有未保存的改动
要继续吗?") 607 | message_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) 608 | message_box.setDefaultButton(QMessageBox.StandardButton.No) 609 | message_box.button(QMessageBox.StandardButton.Yes).setText("确认") 610 | message_box.button(QMessageBox.StandardButton.No).setText("取消") 611 | message_box.setIcon(QMessageBox.Icon.Warning) 612 | 613 | reply = message_box.exec() 614 | if reply == QMessageBox.StandardButton.No: 615 | return 616 | 617 | 618 | QDesktopServices.openUrl(QUrl.fromLocalFile(getConfigFilePath())) 619 | # wait for a while to let QDesktopServices finish his job 620 | QTimer.singleShot(1000, self.close) 621 | # manual edit button end # 622 | 623 | # keybind # 624 | def onKeybindButtonCliecked(self): 625 | self.hide() 626 | 627 | keybinds_dict = self.keybinds 628 | if not keybinds_dict: 629 | keybinds_dict = self.config 630 | 631 | self.overlay_keybinding = keyBindingPanel(self, keybinds_dict) 632 | self.overlay_keybinding.destroyed.connect(self.onOverlayKeybindingPanelDestroyed) 633 | self.overlay_keybinding.show() 634 | 635 | def onKeybindingOk(self): 636 | keybinds_dict = {} 637 | for k, v in self.overlay_keybinding.textEdits.items(): 638 | keybinds_dict[k] = v.toPlainText() 639 | self.keybinds = keybinds_dict 640 | 641 | self.overlay_keybinding.close() 642 | 643 | def onOverlayKeybindingPanelDestroyed(self): 644 | if self.isVisible(): 645 | return 646 | self.show() 647 | # keybind end # 648 | 649 | # resize panel # 650 | def onResizeTestButtonCliecked(self): 651 | path = './temp/gui_resize_test_screenshot.png' 652 | config = { 653 | "LEFT": self.size_x_spinbox.value(), 654 | "TOP": self.size_y_spinbox.value(), 655 | "RIGHT": self.size_w_spinbox.value(), 656 | "BOTTOM": self.size_h_spinbox.value() 657 | } 658 | 659 | capture_screenshot(path) 660 | crop_image(path,path,config=config) 661 | QDesktopServices.openUrl(QUrl.fromLocalFile(path)) 662 | 663 | def onResizeButtonCliecked(self): 664 | 665 | # EDIT: well we just find out that this software can not even run on wayland, and this tips is useless now 666 | # can not get overlayWindow position at wayland environment, so this feat is fucked on wayland 667 | #if os.environ.get('WAYLAND_DISPLAY') is not None: 668 | # QMessageBox.critical(self, 'Error', 'Wayland环境下无法使用此功能', QMessageBox.StandardButton.Ok) 669 | # return 670 | 671 | self.hide() 672 | 673 | 674 | x, y, w, h = int(self.size_x_spinbox.value()), int(self.size_y_spinbox.value()), int(self.size_w_spinbox.value()), int(self.size_h_spinbox.value()) 675 | 676 | # fix screen dpi scaling problem 677 | if os.name == "nt": 678 | point = QPoint(x, y) 679 | screen = QGuiApplication.screenAt(point) 680 | screen = self.windowHandle().screen() 681 | # defensive fix 682 | if screen is None: 683 | QMessageBox.critical(self, 'QMessageBox', '无法获取当前屏幕信息?!\n将不会处理屏幕缩放因子所带来的影响', QMessageBox.StandardButton.Ok) 684 | scale = 1.0 685 | else: 686 | scale = screen.devicePixelRatio() 687 | x = round(x / scale) 688 | y = round(y / scale) 689 | w = round(w / scale) 690 | h = round(h / scale) 691 | 692 | # make absolute position to window size 693 | w, h = w - x, h - y 694 | 695 | self.overlay_resize = resizePanel(self, int(x), int(y), int(w), int(h)) 696 | self.overlay_resize.destroyed.connect(self.onOverlayResizePanelDestroyed) 697 | self.overlay_resize.show() 698 | 699 | def onResizeSaved(self): 700 | x, y, w, h = self.overlay_resize.geometry().getRect() 701 | # defensive fix 702 | if w is None or h is None or x is None or y is None: 703 | QMessageBox.critical(self, 'QMessageBox', '无法从交互式面板获取框选区域?!\n当前未进行任何改动', QMessageBox.StandardButton.Ok) 704 | return 705 | # change window size to absolute position 706 | w, h = x + w, y + h 707 | 708 | # EDIT: haha i cant fix that, no help at all 709 | # linux wayland desktop defensive fix, fucking wayland destroy everything 710 | #if x == 0 and y == 0: 711 | # point = self.overlay_resize.windowHandle().screen().geometry().topLeft() 712 | # x = point.x() 713 | # y = point.y() 714 | # print("wayland defensive fix, x value:"+ str(x) +" y value:"+ str(y)) 715 | 716 | 717 | # fix screen dpi scaling problem 718 | if os.name == "nt": 719 | screen = self.overlay_resize.windowHandle().screen() 720 | # defensive fix 721 | if screen is None: 722 | QMessageBox.critical(self, 'QMessageBox', '无法获取当前屏幕信息?!\n将不会处理屏幕缩放因子所带来的影响', QMessageBox.StandardButton.Ok) 723 | scale = 1.0 724 | else: 725 | scale = screen.devicePixelRatio() 726 | x = round(x * scale) 727 | y = round(y * scale) 728 | w = round(w * scale) 729 | h = round(h * scale) 730 | 731 | self.size_x_spinbox.setValue(x) 732 | self.size_y_spinbox.setValue(y) 733 | self.size_w_spinbox.setValue(w) 734 | self.size_h_spinbox.setValue(h) 735 | 736 | self.overlay_resize.close() 737 | 738 | def onOverlayResizePanelDestroyed(self): 739 | if self.isVisible(): 740 | return 741 | self.show() 742 | # resize panel end # 743 | 744 | 745 | def showEvent(self, event): 746 | self.is_closed = False 747 | 748 | def closeEvent(self, event): 749 | self.is_closed = True 750 | self.hotkeyMgr.start() 751 | super().closeEvent(event) 752 | 753 | # class from old tkGui(early nuked), im too lazy so i didnt change the api format # 754 | class settingsGUI(QObject): 755 | 756 | exit_signal = pyqtSignal() 757 | 758 | def __init__(self, config: dict, hotkeyManager: GlobalHotKeyManager): 759 | super().__init__() 760 | 761 | self.config = config 762 | self.hotkeyMgr = hotkeyManager 763 | 764 | self.app = QApplication([]) 765 | self.app.setQuitOnLastWindowClosed(False) 766 | self.exit_signal.connect(self.app.quit) 767 | 768 | self.window = settingPanel(self.app, self.config, self.hotkeyMgr) 769 | 770 | def execute(self): 771 | self.app.exec() 772 | 773 | def start_qt_widget(self): 774 | 775 | if not self.window.is_closed: 776 | return 777 | 778 | self.window.show() 779 | self.window.raise_() 780 | self.window.activateWindow() 781 | 782 | # will restart it when window closed, see settingPanel.closeEvent() 783 | self.hotkeyMgr.stop() 784 | 785 | if os.environ.get('WAYLAND_DISPLAY') is not None: 786 | QMessageBox.critical(self.window, 'QMessageBox', '此软件无法在Wayland环境下使用\n详见:\nhttps://github.com/BoboTiG/python-mss/issues/155', QMessageBox.StandardButton.Ok) 787 | 788 | def open_settings_gui(self): 789 | # make sure is running on main thread 790 | QTimer.singleShot(0, self.start_qt_widget) 791 | 792 | def startWithProgram(self): 793 | if self.config.get("START_GUI_WITH_PROGRAM", "True").upper() == "TRUE": 794 | self.open_settings_gui() 795 | 796 | def quit(self): 797 | global kbl 798 | 799 | self.exit_signal.emit() 800 | 801 | if not kbl is None: 802 | kbl.stop() 803 | -------------------------------------------------------------------------------- /static/webctl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PERSONAL HELLPAD 8 | 640 | 641 | 642 | 643 |
644 |
645 |
646 |
战略配备
647 |
检测中...
648 |
649 |
650 |
651 |
652 |
653 |
执行中
654 |
655 |
656 | 657 |
658 |
659 | 660 | 661 | 662 | 663 |
664 | 665 |
666 | 667 | 668 |
669 |
670 |
671 | 672 |
673 | 674 | 1020 | 1021 | 1022 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------

', '', 17 | ; '', '', '', '', '', '', '', '', '', 18 | ; '<0>', '<1>', '<2>', '<3>', '<4>', '<5>', '<6>', '<7>', '<8>', '<9>', 19 | ; '', '', '', '', '', 20 | ; '', '', '', '', '', 21 | ; '', '', '', '', '', 22 | ; '', '', '', '', '', 23 | ; '', '', '', '', 24 | ; '', '', '', '', '', 25 | ; '', '<`>', '<~>', '', '<@>', '<#>', '<$>', '<%>', '<^>', '<&>', '<*>', 26 | ; '<(>', '<)>', '<_>', '<->', '<=>', '<\>', '<;>', '<:>', '<[>', '<{>', '<]>', '<}>', 27 | ; '', '', "<'>", '<">', '<,>', '<<>', '<', '<.>', '<>>' 28 | ; 使用+连接同组多个按键,表示同时按下 29 | ; 使用|连接多组按键,表示按下任意一组 30 | ; 识别按键 31 | OCRKEY=+<->|+<-> 32 | ; 打开设置面板 33 | SETTINGKEY=+<=>|+<=> 34 | ; 战备1 35 | SKEY1=+<1>|+<1> 36 | ; 战备2 37 | SKEY2=+<2>|+<2> 38 | ; 战备3 39 | SKEY3=+<3>|+<3> 40 | ; 战备4 41 | SKEY4=+<4>|+<4> 42 | ; 战备5 43 | SKEY5=+<5>|+<5> 44 | ; 战备6 45 | SKEY6=+<6>|+<6> 46 | ; 战备7 47 | SKEY7=+<7>|+<7> 48 | ; 战备8 49 | SKEY8=+<8>|+<8> 50 | ; 战备9 51 | SKEY9=+<9>|+<9> 52 | ; 战备10 53 | SKEY10=+<0>|+<0> 54 | 55 | ; 干扰器优化模式战备按键,每次使用战备都会重新识别,不会输出中间文件,不会实时更新设置.由于每次使用战备都会重新识别,所以还是会比识别完成后使用慢,但会比先识别后使用快. 56 | SKEYANDOCR1=+|+ 57 | SKEYANDOCR2=+|+ 58 | SKEYANDOCR3=+|+ 59 | SKEYANDOCR4=+|+ 60 | SKEYANDOCR5=+|+ 61 | SKEYANDOCR6=+|+ 62 | SKEYANDOCR7=+|+ 63 | SKEYANDOCR8=+|+ 64 | SKEYANDOCR9=+|+ 65 | SKEYANDOCR10=+|+ 66 | 67 | ; 打开战备面板 68 | ACTIVATION= 69 | ; 设置战备按键 70 | W= 71 | A= 72 | S= 73 | D= 74 | 75 | ; 战备面板识别区域 76 | LEFT=30 77 | TOP=20 78 | RIGHT=220 79 | BOTTOM=400 80 | 81 | ; 二值化相关设置 82 | THRESHOLD=30 83 | COLORS=#DAC177,#DF7567,#50AFC8,#74A15F,#BEBEBE,#BAB9A1,#E4D0AA 84 | 85 | ; 按键触发速度设置 86 | DELAY_MIN=0.03 87 | DELAY_MAX=0.08 88 | 89 | ; GUI相关设置 90 | ; GUI是否应该随着程序启动 91 | START_GUI_WITH_PROGRAM=True 92 | ; WEBUI设置 93 | WEB_GUI_PORT=80 94 | WEB_GUI_HOST=all 95 | ''' 96 | 97 | def getConfigFilePath(filename: str = 'config.ini') -> str: 98 | """ 99 | 获取指定配置文件的路径 100 | 101 | Args: 102 | -filename: str 103 | 104 | Returns: 105 | -str 106 | """ 107 | local_path = f'./local/{filename}' 108 | file_path = local_path if os.path.exists(local_path) else f'./{filename}' 109 | return file_path 110 | 111 | def getDefaultConfigDict() -> dict: 112 | """ 113 | 获取默认配置文件并返回一个字典 114 | 115 | Returns: 116 | - dict 117 | """ 118 | result = {} 119 | # 遍历default_config每一行 120 | for line in default_config.splitlines(): 121 | # 如果行不为空,且不以;开头 122 | if line and not line.startswith(";"): 123 | # 用等号分割键和值 124 | key, value = line.split("=", 1) 125 | # 将键值对添加到字典中 126 | result[key] = value 127 | return result 128 | 129 | def getConfigDict() -> dict: 130 | """ 131 | 读取指定配置文件并返回一个字典 132 | 133 | Returns: 134 | - dict 135 | """ 136 | file_path = getConfigFilePath() 137 | # 如果文件不存在,则创建一个默认的配置文件 138 | if not os.path.exists(file_path): 139 | with open(file_path, "w", encoding='utf-8') as f: 140 | f.write(default_config) 141 | default_config_dict = getDefaultConfigDict() 142 | # 创建一个空字典 143 | result = {} 144 | # 打开文件 145 | with open(file_path, "r", encoding='utf-8') as f: 146 | # 遍历文件的每一行 147 | for line in f: 148 | # 去掉行尾的换行符 149 | line = line.strip() 150 | # 如果行不为空,且不以;开头 151 | if line and not line.startswith(";"): 152 | # 用等号分割键和值 153 | key, value = line.split("=", 1) 154 | # 将键值对添加到字典中 155 | result[key] = value 156 | # 遍历default_config_dict,不在result中的键值对添加到result中 157 | for key, value in default_config_dict.items(): 158 | if key not in result: 159 | result[key] = value 160 | # 返回字典 161 | return result 162 | 163 | def saveConfigDict(config: dict) -> None: 164 | """ 165 | 将字典保存到指定配置文件中 166 | 167 | Args: 168 | -config: dict 169 | -filename: str 170 | """ 171 | file_path = getConfigFilePath() 172 | result = '' 173 | old_config_str = '' 174 | # 载入旧设置 175 | with open(file_path, "r", encoding='utf-8') as f: 176 | old_config_str = f.read() 177 | # 生成新设置 178 | # 记录已替换的设置 179 | replaced_keys = set() 180 | # 遍历old_config_str每一行 181 | for line in old_config_str.splitlines(): 182 | # 如果行不为空,且不以;开头 183 | if line and not line.startswith(";"): 184 | # 用等号分割键和值 185 | key, value = line.split("=", 1) 186 | # 如果key在config中,则将value替换为config中的值 187 | value = config.get(key, value) 188 | new_line = f'{key}={value}\n' 189 | result += new_line 190 | replaced_keys.add(key) 191 | else: 192 | result += line + '\n' 193 | # 如果有未替换的设置,则添加到result中 194 | # 先判断是否有未替换的设置,如果有添加注释 195 | if len([i for i in config.keys() if i not in replaced_keys]) > 0: 196 | result += '\n;自动生成设置\n;Auto generated settings\n' 197 | for key, value in config.items(): 198 | if key not in replaced_keys: 199 | result += f'{key}={value}\n' 200 | # 将result写入文件 201 | with open(file_path, "w", encoding='utf-8') as f: 202 | f.write(result) 203 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import json 3 | import random 4 | import sys 5 | import threading 6 | import os 7 | import shutil 8 | import time 9 | import logging 10 | from util.Util import run_in_thread 11 | from util.globalHotKeyManager import GlobalHotKeyManager, c 12 | from util.imageProcessing import arrow_str, binarize_image, capture_screenshot, crop_image, fast_arrow, process_images, read_bmp_to_png_base64, resize_image, screenshot_icon_crop_to_base64, split_image 13 | from util.settingGUI import settingsGUI 14 | from util.loadSetting import getConfigDict 15 | from util.SystemTrayIcon import SystemTrayIcon 16 | 17 | from util.webui import FastAPIServer 18 | try: 19 | from winsound import Beep 20 | except ModuleNotFoundError: 21 | logging.warning('winsound not found, beep will not work') 22 | 23 | version = 'v1.0.0' 24 | 25 | def checkPath(): 26 | """确保工作路径正确""" 27 | # 获取当前工作路径 28 | current_work_dir = os.getcwd() 29 | logging.debug(f"当前工作路径:{current_work_dir}") 30 | 31 | # 获取当前文件所在路径 32 | current_file_dir = os.path.dirname(__file__) 33 | logging.debug(f"文件所在路径:{current_file_dir}") 34 | # 如果文件所在路径末尾是(_internal),跳转到上一级 35 | if '_internal' == current_file_dir[-9:]: 36 | current_file_dir = current_file_dir[:-9] 37 | logging.debug('internal') 38 | logging.debug(f"文件所在路径:{current_file_dir}") 39 | 40 | # 如果工作路径不是文件所在路径,切换到文件所在路径 41 | if current_work_dir != current_file_dir: 42 | os.chdir(current_file_dir) 43 | logging.debug("已切换到文件所在路径。") 44 | 45 | 46 | def checkDir(path='./temp'): 47 | """检查目录是否存在,不存在则创建""" 48 | if os.path.exists(path): 49 | shutil.rmtree(path) 50 | os.makedirs(path) 51 | 52 | 53 | @run_in_thread 54 | def di(m = 0): 55 | try: 56 | match m: 57 | case 0: 58 | # 开始提示音 59 | Beep(800, 100) 60 | case 1: 61 | # 结束提示音 62 | Beep(500, 50) 63 | time.sleep(0.01) 64 | Beep(500, 50) 65 | case 2: 66 | # 警告提示音 67 | Beep(400, 80) 68 | case 3: 69 | # 错误提示音 70 | Beep(200, 1000) 71 | except Exception as e: 72 | logging.debug(f'beep error: {e}') 73 | 74 | def arrow_merge(arrow_original_s,arrow_default_s): 75 | arrow_default_l = arrow_default_s.split('\n') 76 | if not arrow_original_s: 77 | return arrow_default_l 78 | arrow_original_l = arrow_original_s.split('\n') 79 | arrow_processed_l = [] 80 | for i in range(len(arrow_original_l)): 81 | line = arrow_original_l[i] 82 | # 如果是空的,查找arrow_default_l中有没有,有就采用arrow_default_l的值 83 | if not line or len(line) <= 2: 84 | if len(arrow_default_l) > i and arrow_default_l[i]: 85 | arrow_processed_l.append(arrow_default_l[i]) 86 | else: 87 | arrow_processed_l.append(line) 88 | else: 89 | arrow_processed_l.append(line) 90 | # 如果arrow_original_l比arrow_default_l少,则用arrow_default_l的值补齐 91 | if len(arrow_original_l) < len(arrow_default_l): 92 | arrow_processed_l += arrow_default_l[len(arrow_original_l):] 93 | return arrow_processed_l 94 | 95 | 96 | file_lock = threading.Lock() 97 | 98 | hotkeyOCR_is_running = False 99 | hotkeyOCR_lock = threading.Lock() 100 | @run_in_thread 101 | def hotkeyOCR(): 102 | global hotkeyOCR_is_running 103 | with hotkeyOCR_lock: 104 | if hotkeyOCR_is_running: 105 | di(2) 106 | return 107 | hotkeyOCR_is_running = True 108 | try: 109 | checkDir() 110 | logging.debug('===开始识别===') 111 | start_time = time.time() 112 | capture_screenshot() 113 | di() 114 | crop_image() 115 | resize_image() 116 | 117 | binarize_image() 118 | split_image() 119 | process_images() 120 | arrow_original_s = arrow_str() 121 | if len(arrow_original_s) < 8: 122 | raise Exception(arrow_original_s) 123 | with file_lock: 124 | with open('./temp/arrow_original.txt', 'w') as f: 125 | f.write(arrow_original_s) 126 | with open('./defaultArrow.txt', 'r') as f: 127 | arrow_default_s = f.read() 128 | arrow_processed_l = arrow_merge(arrow_original_s,arrow_default_s) 129 | with file_lock: 130 | with open('./temp/arrow.txt', 'w') as f: 131 | f.write('\n'.join(arrow_processed_l)) 132 | srv_set_code(arrow_processed_l) 133 | 134 | logging.debug(f'耗时: {time.time() - start_time} 秒') 135 | logging.debug('===识别结束===') 136 | di(1) 137 | except Exception as e: 138 | logging.debug(f'识别失败: {e}') 139 | di(3) 140 | finally: 141 | with hotkeyOCR_lock: 142 | hotkeyOCR_is_running = False 143 | 144 | 145 | hotkeyother_is_running = False 146 | hotkeyother_lock = threading.Lock() 147 | @run_in_thread 148 | def hotkey_other(num: int, fast_mode = False): 149 | global hotkeyother_is_running, config 150 | with hotkeyother_lock: 151 | if hotkeyother_is_running: 152 | return 153 | hotkeyother_is_running = True 154 | try: 155 | if not fast_mode: 156 | with file_lock: 157 | # 如果temp/arrow.txt存在,则读取并执行 158 | if os.path.exists('./temp/arrow.txt'): 159 | with open('./temp/arrow.txt', 'r') as f: 160 | arrow = f.read().split('\n') 161 | # 如果不存在,则读取defaultArrow.txt 162 | else: 163 | with open('./defaultArrow.txt', 'r') as f: 164 | arrow = f.read().split('\n') 165 | else: 166 | try: 167 | arrow = fast_arrow(config) 168 | if not arrow: 169 | raise Exception('未识别到箭头') 170 | except Exception as e: 171 | logging.debug(f'未识别到箭头: {e}') 172 | # di(2) 173 | with file_lock: 174 | # 如果temp/arrow.txt存在,则读取并执行 175 | if os.path.exists('./temp/arrow.txt'): 176 | with open('./temp/arrow.txt', 'r') as f: 177 | arrow = f.read().split('\n') 178 | else: 179 | arrow = '' 180 | with file_lock: 181 | with open('./defaultArrow.txt', 'r') as f: 182 | default_arrow = f.read() 183 | arrow = arrow_merge(arrow, default_arrow) 184 | 185 | code = arrow[num - 1] 186 | logging.debug(f'执行: {code}') 187 | c(code, config if fast_mode else None) 188 | if fast_mode: 189 | # 写出temp/arrow.txt 190 | with file_lock: 191 | with open('./temp/arrow.txt', 'w') as f: 192 | f.write('\n'.join(arrow)) 193 | except Exception as e: 194 | logging.debug(f'操作失败: {num} {e}') 195 | di(2) 196 | finally: 197 | with hotkeyother_lock: 198 | hotkeyother_is_running = False 199 | 200 | @run_in_thread 201 | def srv_set_code(codes:list[str]): 202 | global srv,config 203 | ''' 204 | code_list: [{'code' : 'WASD', 'imgUrl' : 'data:image/png;base64,xxx','codeImgUrl' : 'data:image/png;base64,xxx'},...,{...}] 205 | ''' 206 | result = [] 207 | with open('./temp/screenshot_icon_point.json', 'r', encoding='utf-8') as f: 208 | code_icon_point = json.loads(f.read()) 209 | for idx,code_line in enumerate(codes): 210 | crop_point = code_icon_point[idx] 211 | line = { 212 | 'code' : code_line, 213 | 'imgUrl' : screenshot_icon_crop_to_base64(crop_point), 214 | 'codeImgUrl' : read_bmp_to_png_base64(f'./temp/split_images/{idx}.bmp') 215 | } 216 | result.append(line) 217 | srv.set_code_list(result) 218 | 219 | def main(): 220 | checkPath() 221 | global config,srv 222 | config = getConfigDict() 223 | hotkeyManager = GlobalHotKeyManager() 224 | GUI = settingsGUI(config, hotkeyManager) 225 | srv = FastAPIServer() 226 | hotkeyManager.auto_register(config, hotkeyOCR, GUI.open_settings_gui, hotkey_other) 227 | hotkeyManager.start() 228 | sti = SystemTrayIcon(GUI, srv.start, srv.stop) 229 | sti_thread = sti.start([GUI.quit]) 230 | 231 | GUI.startWithProgram() 232 | # 会阻塞线程 233 | GUI.execute() 234 | 235 | # 等待sti线程结束后退出 236 | sti_thread.join() 237 | hotkeyManager.stop() 238 | srv.stop() 239 | 240 | def onlySettingGuiMain(): 241 | checkPath() 242 | global config 243 | config = getConfigDict() 244 | hotkeyManager = GlobalHotKeyManager() 245 | GUI = settingsGUI(config, hotkeyManager) 246 | hotkeyManager.auto_register(config, hotkeyOCR, GUI.open_settings_gui, hotkey_other) 247 | GUI.open_settings_gui() 248 | GUI.execute() 249 | 250 | if __name__ == '__main__': 251 | print(f'''helldivers2AutoStratagems {version} Copyright (C) 2025 GDNDZZK 252 | This program comes with ABSOLUTELY NO WARRANTY; for details see LICENSE. 253 | This is free software, and you are welcome to redistribute it under certain conditions. 254 | ''') 255 | 256 | logging.basicConfig( 257 | level=logging.DEBUG, # 设置全局日志级别为 DEBUG 258 | format='%(asctime)s - %(levelname)s - %(message)s', 259 | handlers=[ 260 | logging.StreamHandler(), # 控制台输出 261 | logging.FileHandler('log.txt', mode='w', encoding='utf-8') # 保存到文件 262 | ]) 263 | 264 | # 设置文件日志的级别为 INFO 265 | file_handler = logging.FileHandler('log.txt', mode='w', encoding='utf-8') 266 | file_handler.setLevel(logging.INFO) 267 | file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) 268 | 269 | # 设置控制台日志的级别为 DEBUG 270 | console_handler = logging.StreamHandler() 271 | console_handler.setLevel(logging.DEBUG) 272 | console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) 273 | 274 | # 获取根日志器并添加处理器 275 | logger = logging.getLogger() 276 | logger.handlers = [] # 清空默认处理器 277 | logger.addHandler(console_handler) 278 | logger.addHandler(file_handler) 279 | logging.debug('===开始===') 280 | # 如果参数包含"--only-setting-gui",则只打开设置界面 281 | if '--only-setting-gui' in sys.argv: 282 | onlySettingGuiMain() 283 | else: 284 | main() 285 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | HD2AS Control Panel 8 | 223 | 224 | 225 | 226 |