Compare commits
4 Commits
29999f5599
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62f717dde4 | ||
|
|
b67c5be2f3 | ||
|
|
f7e429a9b3 | ||
|
|
3a90c0fa32 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -174,3 +174,8 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
.idea/
|
||||
|
||||
|
||||
.venv/
|
||||
|
||||
|
||||
66
README.md
66
README.md
@@ -1,2 +1,66 @@
|
||||
# luoluo
|
||||
# LuoLuoTool
|
||||
|
||||
《桃源深处有人家》日常任务挂机工具
|
||||
|
||||
## 简介
|
||||
|
||||
LuoLuoTool 是专为《桃源深处有人家》游戏打造的自动化挂机工具,帮助玩家自动完成日常任务,解放双手。
|
||||
|
||||
- **版本**: 0.1
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🎮 自动捕获游戏窗口
|
||||
- 📋 日常任务自动执行
|
||||
- 🔧 杂项辅助功能
|
||||
- ⏳ 更多功能开发中
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
LuoLuoTool/
|
||||
├── src/
|
||||
│ ├── core/ # 核心功能模块
|
||||
│ │ ├── automation.py
|
||||
│ │ ├── game_window.py
|
||||
│ │ ├── actions.py
|
||||
│ │ └── tasks/ # 任务实现
|
||||
│ ├── ui/ # UI界面
|
||||
│ │ ├── app.py
|
||||
│ │ └── pages/ # 各页面
|
||||
│ ├── utils/ # 工具模块
|
||||
│ └── main.py # 程序入口
|
||||
├── assets/ # 资源文件
|
||||
├── configs/ # 配置文件
|
||||
└── logs/ # 日志文件
|
||||
```
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 运行程序
|
||||
|
||||
```bash
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. 启动游戏《桃源深处有人家》
|
||||
2. 运行 LuoLuoTool
|
||||
3. 在"运行"页面点击"捕获窗口"
|
||||
4. 配置需要执行的任务
|
||||
5. 点击"开始挂机"
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅支持 Windows 系统
|
||||
- 需要以管理员权限运行(用于捕获游戏窗口)
|
||||
- 游戏窗口不能被最小化
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
BIN
assets/images/luoluoTool.ico
Normal file
BIN
assets/images/luoluoTool.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
BIN
assets/images/luoluoTool.png
Normal file
BIN
assets/images/luoluoTool.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 446 KiB |
296
debug_tools.py
Normal file
296
debug_tools.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""
|
||||
调试工具集
|
||||
用于开发和调试时的便捷功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
import win32api
|
||||
import win32gui
|
||||
import win32con
|
||||
from src.core.game_window import GameWindowManager
|
||||
from src.core.input_simulator import InputSimulator
|
||||
|
||||
|
||||
class CoordinateDebugger:
|
||||
"""坐标调试器 - 实时显示鼠标坐标"""
|
||||
|
||||
def __init__(self):
|
||||
self.window = None
|
||||
self.running = False
|
||||
|
||||
def capture_window(self):
|
||||
"""捕获游戏窗口"""
|
||||
self.window = GameWindowManager()
|
||||
if self.window.capture_window():
|
||||
print(f"✅ 已捕获窗口: {self.window.window_title}")
|
||||
print(f" 窗口大小: {self.window.client_size}")
|
||||
return True
|
||||
else:
|
||||
print("❌ 未找到游戏窗口")
|
||||
return False
|
||||
|
||||
def show_realtime_coords(self):
|
||||
"""实时显示鼠标坐标"""
|
||||
print("\n" + "="*60)
|
||||
print("实时坐标显示")
|
||||
print("="*60)
|
||||
print("移动鼠标到游戏窗口内查看坐标")
|
||||
print("按 Ctrl+C 停止\n")
|
||||
|
||||
if not self.window:
|
||||
print("⚠️ 请先捕获窗口 (选项 1)")
|
||||
return
|
||||
|
||||
self.running = True
|
||||
last_pos = None
|
||||
|
||||
try:
|
||||
while self.running:
|
||||
# 获取鼠标屏幕坐标
|
||||
screen_x, screen_y = win32api.GetCursorPos()
|
||||
|
||||
# 获取窗口信息
|
||||
hwnd = self.window.hwnd
|
||||
if not hwnd:
|
||||
print("❌ 窗口已关闭")
|
||||
break
|
||||
|
||||
# 获取窗口客户区位置
|
||||
client_rect = win32gui.GetClientRect(hwnd)
|
||||
client_left, client_top = win32gui.ClientToScreen(hwnd, (client_rect[0], client_rect[1]))
|
||||
|
||||
# 计算窗口相对坐标
|
||||
rel_x = screen_x - client_left
|
||||
rel_y = screen_y - client_top
|
||||
|
||||
# 只在坐标变化时更新显示
|
||||
current_pos = (rel_x, rel_y)
|
||||
if current_pos != last_pos:
|
||||
# 清行并显示新坐标
|
||||
print(f"\r屏幕坐标: ({screen_x:4d}, {screen_y:4d}) | "
|
||||
f"窗口相对: ({rel_x:4d}, {rel_y:4d})", end="", flush=True)
|
||||
last_pos = current_pos
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n已停止")
|
||||
self.running = False
|
||||
|
||||
def test_click_at_current_pos(self):
|
||||
"""在当前鼠标位置测试点击"""
|
||||
print("\n" + "="*60)
|
||||
print("测试点击 - 当前鼠标位置")
|
||||
print("="*60)
|
||||
|
||||
if not self.window:
|
||||
print("⚠️ 请先捕获窗口 (选项 1)")
|
||||
return
|
||||
|
||||
print("3秒后将点击当前鼠标位置...")
|
||||
time.sleep(3)
|
||||
|
||||
# 获取当前鼠标位置
|
||||
screen_x, screen_y = win32api.GetCursorPos()
|
||||
|
||||
# 转换为窗口相对坐标
|
||||
hwnd = self.window.hwnd
|
||||
client_rect = win32gui.GetClientRect(hwnd)
|
||||
client_left, client_top = win32gui.ClientToScreen(hwnd, (client_rect[0], client_rect[1]))
|
||||
rel_x = screen_x - client_left
|
||||
rel_y = screen_y - client_top
|
||||
|
||||
print(f"\n点击位置: 屏幕({screen_x}, {screen_y}) | 窗口相对({rel_x}, {rel_y})")
|
||||
|
||||
# 执行点击
|
||||
sim = InputSimulator(self.window)
|
||||
sim.click(rel_x, rel_y)
|
||||
|
||||
print("✅ 点击完成")
|
||||
|
||||
def test_click_at_coords(self):
|
||||
"""在指定坐标测试点击"""
|
||||
print("\n" + "="*60)
|
||||
print("测试点击 - 指定坐标")
|
||||
print("="*60)
|
||||
|
||||
if not self.window:
|
||||
print("⚠️ 请先捕获窗口 (选项 1)")
|
||||
return
|
||||
|
||||
try:
|
||||
x = int(input("请输入 X 坐标: "))
|
||||
y = int(input("请输入 Y 坐标: "))
|
||||
except ValueError:
|
||||
print("❌ 无效的坐标")
|
||||
return
|
||||
|
||||
print(f"\n将在 ({x}, {y}) 点击")
|
||||
print("3秒后开始...")
|
||||
time.sleep(3)
|
||||
|
||||
sim = InputSimulator(self.window)
|
||||
sim.click(x, y)
|
||||
|
||||
print("✅ 点击完成")
|
||||
|
||||
|
||||
class CoordinateRecorder:
|
||||
"""坐标记录器 - 记录并保存常用坐标"""
|
||||
|
||||
def __init__(self, save_file: str = "coordinates.json"):
|
||||
self.save_file = Path(save_file)
|
||||
self.coordinates = {}
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
"""加载已保存的坐标"""
|
||||
if self.save_file.exists():
|
||||
with open(self.save_file, 'r', encoding='utf-8') as f:
|
||||
self.coordinates = json.load(f)
|
||||
else:
|
||||
self.coordinates = {}
|
||||
|
||||
def save(self):
|
||||
"""保存坐标到文件"""
|
||||
with open(self.save_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.coordinates, f, ensure_ascii=False, indent=2)
|
||||
print(f"✅ 坐标已保存到 {self.save_file}")
|
||||
|
||||
def record_current_pos(self, name: str, window: GameWindowManager):
|
||||
"""记录当前鼠标位置"""
|
||||
screen_x, screen_y = win32api.GetCursorPos()
|
||||
|
||||
hwnd = window.hwnd
|
||||
client_rect = win32gui.GetClientRect(hwnd)
|
||||
client_left, client_top = win32gui.ClientToScreen(hwnd, (client_rect[0], client_rect[1]))
|
||||
|
||||
rel_x = screen_x - client_left
|
||||
rel_y = screen_y - client_top
|
||||
|
||||
self.coordinates[name] = {
|
||||
"x": rel_x,
|
||||
"y": rel_y,
|
||||
"screen_x": screen_x,
|
||||
"screen_y": screen_y,
|
||||
"recorded_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
print(f"✅ 已记录 '{name}': ({rel_x}, {rel_y})")
|
||||
|
||||
def list_coordinates(self):
|
||||
"""列出所有记录的坐标"""
|
||||
if not self.coordinates:
|
||||
print("⚠️ 没有记录的坐标")
|
||||
return
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("已记录的坐标:")
|
||||
print("="*60)
|
||||
for name, coord in self.coordinates.items():
|
||||
print(f" {name}: ({coord['x']}, {coord['y']})")
|
||||
|
||||
def get_coordinate(self, name: str) -> tuple:
|
||||
"""获取指定名称的坐标"""
|
||||
if name in self.coordinates:
|
||||
coord = self.coordinates[name]
|
||||
return (coord['x'], coord['y'])
|
||||
return None
|
||||
|
||||
def delete_coordinate(self, name: str):
|
||||
"""删除指定坐标"""
|
||||
if name in self.coordinates:
|
||||
del self.coordinates[name]
|
||||
print(f"✅ 已删除 '{name}'")
|
||||
else:
|
||||
print(f"❌ 未找到 '{name}'")
|
||||
|
||||
|
||||
def show_menu():
|
||||
"""显示主菜单"""
|
||||
print("\n" + "="*60)
|
||||
print("调试工具菜单")
|
||||
print("="*60)
|
||||
print("1. 捕获游戏窗口")
|
||||
print("2. 实时显示鼠标坐标")
|
||||
print("3. 测试点击 - 当前鼠标位置")
|
||||
print("4. 测试点击 - 指定坐标")
|
||||
print("5. 记录当前坐标")
|
||||
print("6. 查看已记录的坐标")
|
||||
print("7. 删除坐标记录")
|
||||
print("8. 保存坐标到文件")
|
||||
print("0. 退出")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
debugger = CoordinateDebugger()
|
||||
recorder = CoordinateRecorder()
|
||||
|
||||
print("="*60)
|
||||
print("LuoLuoTool 调试工具")
|
||||
print("="*60)
|
||||
print("请以管理员权限运行此脚本")
|
||||
|
||||
while True:
|
||||
show_menu()
|
||||
choice = input("\n请选择功能 (0-8): ").strip()
|
||||
|
||||
if choice == '0':
|
||||
print("\n感谢使用,再见!")
|
||||
break
|
||||
|
||||
elif choice == '1':
|
||||
debugger.capture_window()
|
||||
|
||||
elif choice == '2':
|
||||
debugger.show_realtime_coords()
|
||||
|
||||
elif choice == '3':
|
||||
debugger.test_click_at_current_pos()
|
||||
|
||||
elif choice == '4':
|
||||
debugger.test_click_at_coords()
|
||||
|
||||
elif choice == '5':
|
||||
if not debugger.window:
|
||||
print("⚠️ 请先捕获窗口 (选项 1)")
|
||||
continue
|
||||
name = input("请输入坐标名称 (如: '主界面按钮', '开始游戏'): ").strip()
|
||||
if name:
|
||||
recorder.record_current_pos(name, debugger.window)
|
||||
else:
|
||||
print("❌ 名称不能为空")
|
||||
|
||||
elif choice == '6':
|
||||
recorder.list_coordinates()
|
||||
|
||||
elif choice == '7':
|
||||
recorder.list_coordinates()
|
||||
name = input("请输入要删除的坐标名称: ").strip()
|
||||
if name:
|
||||
recorder.delete_coordinate(name)
|
||||
|
||||
elif choice == '8':
|
||||
recorder.save()
|
||||
|
||||
else:
|
||||
print("❌ 无效选项")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n程序被用户中断")
|
||||
except Exception as e:
|
||||
print(f"\n❌ 发生错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
275
demo_input.py
Normal file
275
demo_input.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
InputSimulator 使用示例
|
||||
演示如何使用输入模拟器进行鼠标点击、滑动和键盘操作
|
||||
|
||||
运行前请确保:
|
||||
1. 游戏《桃源深处有人家》已启动
|
||||
2. 游戏窗口可见(未被最小化)
|
||||
3. 以管理员权限运行此脚本
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from src.core.game_window import GameWindowManager
|
||||
from src.core.input_simulator import InputSimulator
|
||||
|
||||
|
||||
def demo_basic_click():
|
||||
"""演示基础点击"""
|
||||
print("\n" + "="*50)
|
||||
print("演示1: 基础点击")
|
||||
print("="*50)
|
||||
|
||||
# 捕获游戏窗口
|
||||
window = GameWindowManager()
|
||||
if not window.capture_window():
|
||||
print("❌ 未找到游戏窗口,请确保游戏已运行")
|
||||
return False
|
||||
|
||||
print(f"✅ 已捕获窗口: {window.client_size}")
|
||||
|
||||
# 创建模拟器
|
||||
sim = InputSimulator(window)
|
||||
|
||||
# 获取窗口中心坐标
|
||||
size = window.client_size
|
||||
center_x = size[0] // 2
|
||||
center_y = size[1] // 2
|
||||
|
||||
print(f"\n将在窗口中心 ({center_x}, {center_y}) 进行点击演示")
|
||||
print("3秒后开始...")
|
||||
time.sleep(3)
|
||||
|
||||
# 1. 短按点击
|
||||
print("\n1. 短按点击...")
|
||||
sim.click(center_x, center_y)
|
||||
time.sleep(1)
|
||||
|
||||
# 2. 长按1秒
|
||||
print("2. 长按1秒...")
|
||||
sim.click(center_x, center_y, duration=1.0)
|
||||
time.sleep(1)
|
||||
|
||||
# 3. 双击
|
||||
print("3. 双击...")
|
||||
sim.double_click(center_x, center_y)
|
||||
time.sleep(1)
|
||||
|
||||
print("✅ 点击演示完成")
|
||||
return True
|
||||
|
||||
|
||||
def demo_swipe():
|
||||
"""演示滑动操作"""
|
||||
print("\n" + "="*50)
|
||||
print("演示2: 滑动操作")
|
||||
print("="*50)
|
||||
|
||||
window = GameWindowManager()
|
||||
if not window.capture_window():
|
||||
print("❌ 未找到游戏窗口")
|
||||
return False
|
||||
|
||||
sim = InputSimulator(window)
|
||||
size = window.client_size
|
||||
|
||||
# 计算滑动起点和终点
|
||||
start_x = size[0] // 4
|
||||
start_y = size[1] // 2
|
||||
end_x = size[0] * 3 // 4
|
||||
end_y = size[1] // 2
|
||||
|
||||
print(f"\n将从 ({start_x}, {start_y}) 滑动到 ({end_x}, {end_y})")
|
||||
print("3秒后开始...")
|
||||
time.sleep(3)
|
||||
|
||||
# 水平滑动
|
||||
print("\n1. 水平滑动(0.5秒)...")
|
||||
sim.swipe(start_x, start_y, end_x, end_y, duration=0.5)
|
||||
time.sleep(1)
|
||||
|
||||
# 垂直滑动
|
||||
print("2. 垂直滑动(0.5秒)...")
|
||||
sim.swipe(size[0] // 2, size[1] // 4, size[0] // 2, size[1] * 3 // 4, duration=0.5)
|
||||
time.sleep(1)
|
||||
|
||||
# 对角线滑动
|
||||
print("3. 对角线滑动(1秒)...")
|
||||
sim.swipe(100, 100, size[0]-100, size[1]-100, duration=1.0)
|
||||
time.sleep(1)
|
||||
|
||||
print("✅ 滑动演示完成")
|
||||
return True
|
||||
|
||||
|
||||
def demo_keyboard():
|
||||
"""演示键盘操作"""
|
||||
print("\n" + "="*50)
|
||||
print("演示3: 键盘操作")
|
||||
print("="*50)
|
||||
|
||||
window = GameWindowManager()
|
||||
if not window.capture_window():
|
||||
print("❌ 未找到游戏窗口")
|
||||
return False
|
||||
|
||||
sim = InputSimulator(window)
|
||||
|
||||
print("\n3秒后开始键盘演示...")
|
||||
print("注意:请确保游戏窗口可以接收键盘输入")
|
||||
time.sleep(3)
|
||||
|
||||
# 1. 按ESC键(通常用于关闭菜单)
|
||||
print("\n1. 按 ESC 键...")
|
||||
sim.key_press('esc')
|
||||
time.sleep(1)
|
||||
|
||||
# 2. 按空格键
|
||||
print("2. 按空格键...")
|
||||
sim.key_press('space')
|
||||
time.sleep(1)
|
||||
|
||||
# 3. 组合按键示例:按住Ctrl再按A
|
||||
print("3. 组合按键 Ctrl+A...")
|
||||
sim.key_down('ctrl')
|
||||
sim.key_press('a')
|
||||
sim.key_up('ctrl')
|
||||
time.sleep(1)
|
||||
|
||||
print("✅ 键盘演示完成")
|
||||
return True
|
||||
|
||||
|
||||
def demo_game_automation():
|
||||
"""
|
||||
演示游戏自动化场景
|
||||
模拟一个简单的游戏操作流程
|
||||
"""
|
||||
print("\n" + "="*50)
|
||||
print("演示4: 游戏自动化场景")
|
||||
print("="*50)
|
||||
|
||||
window = GameWindowManager()
|
||||
if not window.capture_window():
|
||||
print("❌ 未找到游戏窗口")
|
||||
return False
|
||||
|
||||
sim = InputSimulator(window)
|
||||
size = window.client_size
|
||||
|
||||
print("\n这是一个模拟的游戏自动化流程:")
|
||||
print("1. 打开菜单(点击菜单按钮)")
|
||||
print("2. 选择选项(滑动选择)")
|
||||
print("3. 确认(点击确认按钮)")
|
||||
print("4. 关闭菜单(按ESC)")
|
||||
|
||||
print("\n5秒后开始自动化流程...")
|
||||
time.sleep(5)
|
||||
|
||||
# 步骤1: 点击菜单按钮(假设在右上角)
|
||||
print("\n步骤1: 点击菜单按钮...")
|
||||
menu_x = size[0] - 100
|
||||
menu_y = 100
|
||||
sim.click(menu_x, menu_y)
|
||||
time.sleep(1)
|
||||
|
||||
# 步骤2: 滑动选择选项
|
||||
print("步骤2: 滑动选择选项...")
|
||||
sim.swipe(size[0]//2, size[1]//2, size[0]//2, size[1]//2 - 200, duration=0.3)
|
||||
time.sleep(1)
|
||||
|
||||
# 步骤3: 点击确认按钮
|
||||
print("步骤3: 点击确认按钮...")
|
||||
confirm_x = size[0] // 2
|
||||
confirm_y = size[1] - 150
|
||||
sim.click(confirm_x, confirm_y)
|
||||
time.sleep(1)
|
||||
|
||||
# 步骤4: 关闭菜单
|
||||
print("步骤4: 关闭菜单...")
|
||||
sim.key_press('esc')
|
||||
time.sleep(1)
|
||||
|
||||
print("✅ 自动化流程完成")
|
||||
return True
|
||||
|
||||
|
||||
def check_admin():
|
||||
"""检查是否以管理员权限运行"""
|
||||
import ctypes
|
||||
try:
|
||||
return ctypes.windll.shell32.IsUserAnAdmin()
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("="*50)
|
||||
print("InputSimulator 使用示例")
|
||||
print("="*50)
|
||||
print("\n请确保游戏《桃源深处有人家》已启动")
|
||||
print("游戏窗口需要可见(未被最小化)")
|
||||
|
||||
# 检查管理员权限
|
||||
if not check_admin():
|
||||
print("\n⚠️ 警告:未以管理员权限运行")
|
||||
print("某些功能可能无法正常工作")
|
||||
print("建议:右键点击命令提示符/终端,选择'以管理员身份运行'")
|
||||
print()
|
||||
response = input("是否继续运行?(y/n): ").strip().lower()
|
||||
if response != 'y':
|
||||
print("程序已退出")
|
||||
return
|
||||
|
||||
# 显示菜单
|
||||
while True:
|
||||
print("\n" + "="*50)
|
||||
print("请选择要运行的演示:")
|
||||
print("="*50)
|
||||
print("1. 基础点击演示")
|
||||
print("2. 滑动操作演示")
|
||||
print("3. 键盘操作演示")
|
||||
print("4. 游戏自动化场景演示")
|
||||
print("5. 运行全部演示")
|
||||
print("0. 退出")
|
||||
|
||||
choice = input("\n请输入选项 (0-5): ").strip()
|
||||
|
||||
if choice == '0':
|
||||
print("\n感谢使用,再见!")
|
||||
break
|
||||
elif choice == '1':
|
||||
demo_basic_click()
|
||||
elif choice == '2':
|
||||
demo_swipe()
|
||||
elif choice == '3':
|
||||
demo_keyboard()
|
||||
elif choice == '4':
|
||||
demo_game_automation()
|
||||
elif choice == '5':
|
||||
demo_basic_click()
|
||||
demo_swipe()
|
||||
demo_keyboard()
|
||||
demo_game_automation()
|
||||
print("\n" + "="*50)
|
||||
print("✅ 所有演示已完成")
|
||||
print("="*50)
|
||||
else:
|
||||
print("\n❌ 无效选项,请重新输入")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n程序被用户中断")
|
||||
except Exception as e:
|
||||
print(f"\n❌ 发生错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
47
pyproject.toml
Normal file
47
pyproject.toml
Normal file
@@ -0,0 +1,47 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "LuoLuoTool"
|
||||
version = "0.1"
|
||||
description = "《桃源深处有人家》日常任务挂机工具"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "moweishan", email = ""}
|
||||
]
|
||||
keywords = ["game", "automation", "挂机", "桃源深处有人家"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"customtkinter>=5.2.2",
|
||||
"Pillow>=10.0.0",
|
||||
"pyautogui>=0.9.54",
|
||||
"pywin32>=306",
|
||||
"opencv-python>=4.8.0",
|
||||
"numpy>=1.24.0",
|
||||
"loguru>=0.7.0",
|
||||
]
|
||||
|
||||
[project.gui-scripts]
|
||||
luoluo = "src.main:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://bk.moweishan.top/"
|
||||
Repository = "https://github.com/moweishan/LuoLuoTool"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["src*"]
|
||||
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
# LuoLuoTool 依赖列表
|
||||
|
||||
# ========== UI框架 ==========
|
||||
customtkinter>=5.2.2 # 现代化UI框架
|
||||
Pillow>=10.0.0 # 图像处理
|
||||
|
||||
# ========== 游戏自动化 ==========
|
||||
pyautogui>=0.9.54 # 鼠标键盘模拟
|
||||
pywin32>=306 # Windows API
|
||||
opencv-python>=4.8.0 # 图像识别
|
||||
numpy>=1.24.0 # 数值计算
|
||||
14
src/core/__init__.py
Normal file
14
src/core/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
LuoLuoTool 核心模块
|
||||
游戏自动化控制核心
|
||||
"""
|
||||
|
||||
from .automation import AutomationController
|
||||
from .game_window import GameWindowManager
|
||||
from .actions import GameActions
|
||||
|
||||
__all__ = [
|
||||
"AutomationController",
|
||||
"GameWindowManager",
|
||||
"GameActions",
|
||||
]
|
||||
41
src/core/actions.py
Normal file
41
src/core/actions.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
游戏动作定义
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Optional
|
||||
from src.utils.logger import logger
|
||||
|
||||
from .game_window import GameWindowManager
|
||||
|
||||
|
||||
class GameActions:
|
||||
"""游戏动作执行器"""
|
||||
|
||||
def __init__(self, window_manager: GameWindowManager):
|
||||
self.window = window_manager
|
||||
|
||||
def click(self, x: int, y: int, delay: float = 0.1):
|
||||
"""点击指定坐标"""
|
||||
self.window.click(x, y)
|
||||
time.sleep(delay)
|
||||
|
||||
def click_template(self, template_name: str, timeout: float = 5.0) -> bool:
|
||||
"""点击匹配到的模板图像"""
|
||||
# TODO: 实现模板匹配点击
|
||||
logger.debug(f"尝试点击模板: {template_name}")
|
||||
return False
|
||||
|
||||
def wait(self, seconds: float):
|
||||
"""等待指定时间"""
|
||||
time.sleep(seconds)
|
||||
|
||||
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float = 0.5):
|
||||
"""滑动操作"""
|
||||
# TODO: 实现滑动
|
||||
logger.debug(f"滑动: ({x1}, {y1}) -> ({x2}, {y2})")
|
||||
|
||||
def press_key(self, key: str):
|
||||
"""按下键盘按键"""
|
||||
# TODO: 实现按键
|
||||
logger.debug(f"按键: {key}")
|
||||
87
src/core/automation.py
Normal file
87
src/core/automation.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
自动化控制主类
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional, Callable
|
||||
from src.utils.logger import logger
|
||||
|
||||
from .game_window import GameWindowManager
|
||||
from .actions import GameActions
|
||||
|
||||
|
||||
class AutomationController:
|
||||
"""自动化控制器"""
|
||||
|
||||
def __init__(self):
|
||||
self.window_manager = GameWindowManager()
|
||||
self.actions = GameActions(self.window_manager)
|
||||
|
||||
self._running = False
|
||||
self._paused = False
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._callback: Optional[Callable] = None
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
return self._running
|
||||
|
||||
@property
|
||||
def is_paused(self) -> bool:
|
||||
return self._paused
|
||||
|
||||
def set_callback(self, callback: Callable):
|
||||
"""设置状态回调函数"""
|
||||
self._callback = callback
|
||||
|
||||
def _notify(self, message: str):
|
||||
"""通知UI更新"""
|
||||
logger.info(message)
|
||||
if self._callback:
|
||||
self._callback(message)
|
||||
|
||||
def start(self):
|
||||
"""开始自动化"""
|
||||
if self._running:
|
||||
return
|
||||
|
||||
if not self.window_manager.is_window_captured:
|
||||
self._notify("请先捕获游戏窗口")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._paused = False
|
||||
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
||||
self._thread.start()
|
||||
self._notify("自动化已启动")
|
||||
|
||||
def stop(self):
|
||||
"""停止自动化"""
|
||||
self._running = False
|
||||
self._paused = False
|
||||
if self._thread:
|
||||
self._thread.join(timeout=2)
|
||||
self._notify("自动化已停止")
|
||||
|
||||
def pause(self):
|
||||
"""暂停/继续"""
|
||||
if not self._running:
|
||||
return
|
||||
self._paused = not self._paused
|
||||
status = "已暂停" if self._paused else "已继续"
|
||||
self._notify(f"自动化{status}")
|
||||
|
||||
def _run_loop(self):
|
||||
"""主运行循环"""
|
||||
while self._running:
|
||||
if self._paused:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
try:
|
||||
# TODO: 实现具体的任务执行逻辑
|
||||
time.sleep(0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"运行错误: {e}")
|
||||
self._notify(f"错误: {e}")
|
||||
282
src/core/game_window.py
Normal file
282
src/core/game_window.py
Normal file
@@ -0,0 +1,282 @@
|
||||
"""
|
||||
游戏窗口管理
|
||||
通过 exe 文件名或窗口标题获取游戏窗口
|
||||
"""
|
||||
|
||||
import os
|
||||
import win32gui
|
||||
import win32process
|
||||
import psutil
|
||||
from typing import Optional, Tuple
|
||||
from src.utils.logger import logger
|
||||
|
||||
|
||||
class GameWindowManager:
|
||||
"""游戏窗口管理器"""
|
||||
|
||||
# 游戏配置
|
||||
GAME_EXE_NAME = "ycgame.exe" # 游戏exe文件名
|
||||
GAME_WINDOW_TITLE = "桃源深处有人家" # 游戏窗口标题
|
||||
|
||||
def __init__(self):
|
||||
self._hwnd: Optional[int] = None
|
||||
self._window_rect: Tuple[int, int, int, int] = (0, 0, 0, 0)
|
||||
self._process_id: Optional[int] = None
|
||||
|
||||
@property
|
||||
def is_window_captured(self) -> bool:
|
||||
"""是否已捕获窗口"""
|
||||
if self._hwnd is None:
|
||||
return False
|
||||
return win32gui.IsWindow(self._hwnd)
|
||||
|
||||
@property
|
||||
def hwnd(self) -> Optional[int]:
|
||||
"""窗口句柄"""
|
||||
return self._hwnd
|
||||
|
||||
@property
|
||||
def process_id(self) -> Optional[int]:
|
||||
"""进程ID"""
|
||||
return self._process_id
|
||||
|
||||
@property
|
||||
def window_rect(self) -> Tuple[int, int, int, int]:
|
||||
"""窗口矩形 (left, top, right, bottom)"""
|
||||
if self.is_window_captured:
|
||||
self._window_rect = win32gui.GetWindowRect(self._hwnd)
|
||||
return self._window_rect
|
||||
|
||||
@property
|
||||
def client_size(self) -> Tuple[int, int]:
|
||||
"""客户端区域大小 (width, height)"""
|
||||
if not self.is_window_captured:
|
||||
return (0, 0)
|
||||
left, top, right, bottom = self.window_rect
|
||||
return (right - left, bottom - top)
|
||||
|
||||
def find_window_by_exe(self, exe_name: str = None) -> bool:
|
||||
"""
|
||||
通过exe文件名查找游戏窗口
|
||||
|
||||
Args:
|
||||
exe_name: exe文件名,默认使用 GAME_EXE_NAME
|
||||
|
||||
Returns:
|
||||
bool: 是否找到窗口
|
||||
"""
|
||||
if exe_name is None:
|
||||
exe_name = self.GAME_EXE_NAME
|
||||
|
||||
try:
|
||||
# 获取所有进程
|
||||
hwnd_list = []
|
||||
|
||||
def enum_windows_callback(hwnd, extra):
|
||||
if not win32gui.IsWindowVisible(hwnd):
|
||||
return True
|
||||
|
||||
try:
|
||||
# 获取窗口对应的进程ID
|
||||
_, pid = win32process.GetWindowThreadProcessId(hwnd)
|
||||
|
||||
# 获取进程信息
|
||||
try:
|
||||
process = psutil.Process(pid)
|
||||
process_exe = process.exe()
|
||||
|
||||
# 检查exe文件名是否精确匹配
|
||||
# 使用 os.path.basename 获取文件名,避免路径包含目标名称的误匹配
|
||||
process_exe_name = os.path.basename(process_exe).lower()
|
||||
target_exe_name = exe_name.lower()
|
||||
|
||||
if process_exe_name == target_exe_name:
|
||||
extra.append((hwnd, pid, process_exe))
|
||||
logger.debug(f"[窗口枚举] 找到匹配窗口: hwnd={hwnd}, pid={pid}, exe={process_exe}")
|
||||
except psutil.NoSuchProcess:
|
||||
# 进程已结束,忽略
|
||||
logger.debug(f"[窗口枚举] 进程 {pid} 已不存在,跳过")
|
||||
except psutil.AccessDenied:
|
||||
# 无权限访问进程,忽略
|
||||
logger.debug(f"[窗口枚举] 无权限访问进程 {pid},跳过")
|
||||
except psutil.Error as e:
|
||||
# 其他psutil错误
|
||||
logger.warning(f"[窗口枚举] 获取进程 {pid} 信息失败: {e}")
|
||||
|
||||
except win32process.error as e:
|
||||
# win32process 错误
|
||||
logger.warning(f"[窗口枚举] 获取窗口 {hwnd} 的进程ID失败: {e}")
|
||||
except OSError as e:
|
||||
# 系统错误
|
||||
logger.warning(f"[窗口枚举] 处理窗口 {hwnd} 时发生系统错误: {e}")
|
||||
|
||||
return True
|
||||
|
||||
# 枚举所有窗口
|
||||
win32gui.EnumWindows(enum_windows_callback, hwnd_list)
|
||||
|
||||
if hwnd_list:
|
||||
# 优先选择标题匹配的窗口
|
||||
for hwnd, pid, exe_path in hwnd_list:
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
if self.GAME_WINDOW_TITLE in title:
|
||||
self._hwnd = hwnd
|
||||
self._process_id = pid
|
||||
self._window_rect = win32gui.GetWindowRect(hwnd)
|
||||
logger.info(f"通过exe找到游戏窗口: {hwnd}, PID: {pid}, 大小: {self.client_size}, 标题: {title}")
|
||||
return True
|
||||
|
||||
# 如果没有标题匹配,使用第一个找到的窗口
|
||||
hwnd, pid, exe_path = hwnd_list[0]
|
||||
self._hwnd = hwnd
|
||||
self._process_id = pid
|
||||
self._window_rect = win32gui.GetWindowRect(hwnd)
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
logger.info(f"通过exe找到游戏窗口(备用): {hwnd}, PID: {pid}, 大小: {self.client_size}, 标题: {title}")
|
||||
return True
|
||||
|
||||
logger.warning(f"未找到exe为 {exe_name} 的游戏窗口")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"通过exe查找窗口失败: {e}")
|
||||
return False
|
||||
|
||||
def find_window_by_title(self, title: str = None) -> bool:
|
||||
"""
|
||||
通过窗口标题查找游戏窗口
|
||||
|
||||
Args:
|
||||
title: 窗口标题,默认使用 GAME_WINDOW_TITLE
|
||||
|
||||
Returns:
|
||||
bool: 是否找到窗口
|
||||
"""
|
||||
if title is None:
|
||||
title = self.GAME_WINDOW_TITLE
|
||||
|
||||
try:
|
||||
# 先尝试精确匹配
|
||||
hwnd = win32gui.FindWindow(None, title)
|
||||
|
||||
# 如果没找到,尝试模糊匹配
|
||||
if hwnd == 0:
|
||||
def callback(hwnd, extra):
|
||||
if win32gui.IsWindowVisible(hwnd):
|
||||
window_title = win32gui.GetWindowText(hwnd)
|
||||
if title in window_title:
|
||||
extra.append(hwnd)
|
||||
return True
|
||||
|
||||
windows = []
|
||||
win32gui.EnumWindows(callback, windows)
|
||||
if windows:
|
||||
hwnd = windows[0]
|
||||
|
||||
if hwnd != 0:
|
||||
self._hwnd = hwnd
|
||||
self._window_rect = win32gui.GetWindowRect(hwnd)
|
||||
|
||||
# 获取进程ID
|
||||
try:
|
||||
_, self._process_id = win32process.GetWindowThreadProcessId(hwnd)
|
||||
except Exception:
|
||||
self._process_id = None
|
||||
|
||||
logger.info(f"通过标题找到游戏窗口: {hwnd}, PID: {self._process_id}, 大小: {self.client_size}")
|
||||
return True
|
||||
|
||||
logger.warning(f"未找到标题为 {title} 的游戏窗口")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"通过标题查找窗口失败: {e}")
|
||||
return False
|
||||
|
||||
def find_window(self, use_exe: bool = True) -> bool:
|
||||
"""
|
||||
查找游戏窗口(综合方法)
|
||||
|
||||
Args:
|
||||
use_exe: 优先使用exe名查找,失败后再尝试标题查找
|
||||
|
||||
Returns:
|
||||
bool: 是否找到窗口
|
||||
"""
|
||||
if use_exe:
|
||||
# 先尝试通过exe查找
|
||||
if self.find_window_by_exe():
|
||||
return True
|
||||
# 失败后尝试标题查找
|
||||
logger.info("通过exe查找失败,尝试通过标题查找")
|
||||
return self.find_window_by_title()
|
||||
else:
|
||||
return self.find_window_by_title()
|
||||
|
||||
def capture_window(self, use_exe: bool = True) -> bool:
|
||||
"""
|
||||
捕获游戏窗口
|
||||
|
||||
Args:
|
||||
use_exe: 是否优先使用exe名查找
|
||||
|
||||
Returns:
|
||||
bool: 是否成功捕获
|
||||
"""
|
||||
return self.find_window(use_exe=use_exe)
|
||||
|
||||
def bring_to_front(self):
|
||||
"""将窗口置前"""
|
||||
if self.is_window_captured:
|
||||
win32gui.SetForegroundWindow(self._hwnd)
|
||||
|
||||
def click(self, x: int, y: int):
|
||||
"""在窗口内点击"""
|
||||
if not self.is_window_captured:
|
||||
return
|
||||
|
||||
left, top, _, _ = self.window_rect
|
||||
# 转换为屏幕坐标
|
||||
screen_x = left + x
|
||||
screen_y = top + y
|
||||
|
||||
# TODO: 使用pyautogui或win32api发送点击
|
||||
logger.debug(f"点击坐标: ({screen_x}, {screen_y})")
|
||||
|
||||
def get_window_info(self) -> dict:
|
||||
"""
|
||||
获取窗口详细信息
|
||||
|
||||
Returns:
|
||||
dict: 窗口信息字典
|
||||
"""
|
||||
if not self.is_window_captured:
|
||||
return {
|
||||
"captured": False,
|
||||
"hwnd": None,
|
||||
"title": None,
|
||||
"process_id": None,
|
||||
"exe_path": None,
|
||||
"rect": None,
|
||||
"size": None
|
||||
}
|
||||
|
||||
title = win32gui.GetWindowText(self._hwnd)
|
||||
exe_path = None
|
||||
|
||||
if self._process_id:
|
||||
try:
|
||||
process = psutil.Process(self._process_id)
|
||||
exe_path = process.exe()
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
|
||||
return {
|
||||
"captured": True,
|
||||
"hwnd": self._hwnd,
|
||||
"title": title,
|
||||
"process_id": self._process_id,
|
||||
"exe_path": exe_path,
|
||||
"rect": self.window_rect,
|
||||
"size": self.client_size
|
||||
}
|
||||
629
src/core/input_simulator.py
Normal file
629
src/core/input_simulator.py
Normal file
@@ -0,0 +1,629 @@
|
||||
"""
|
||||
输入模拟器
|
||||
提供鼠标和键盘的模拟操作,包括点击、长按、滑动等
|
||||
使用 SendInput 模拟真实硬件输入,绕过游戏防护
|
||||
|
||||
# 捕获窗口
|
||||
window = GameWindowManager()
|
||||
window.capture_window()
|
||||
|
||||
# 创建模拟器
|
||||
sim = InputSimulator(window)
|
||||
|
||||
# 点击操作(统一使用 SendInput)
|
||||
sim.click(x, y) # 短按
|
||||
sim.click(x, y, duration=1.0) # 长按
|
||||
sim.double_click(x, y) # 双击
|
||||
sim.swipe(x1, y1, x2, y2, duration=0.5) # 滑动
|
||||
sim.key_press('esc') # 按键
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
from typing import Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
from src.utils.logger import logger
|
||||
from src.core.game_window import GameWindowManager
|
||||
|
||||
|
||||
# Windows API 常量
|
||||
INPUT_MOUSE = 0
|
||||
INPUT_KEYBOARD = 1
|
||||
|
||||
# 鼠标事件标志
|
||||
MOUSEEVENTF_MOVE = 0x0001
|
||||
MOUSEEVENTF_ABSOLUTE = 0x8000
|
||||
MOUSEEVENTF_LEFTDOWN = 0x0002
|
||||
MOUSEEVENTF_LEFTUP = 0x0004
|
||||
MOUSEEVENTF_RIGHTDOWN = 0x0008
|
||||
MOUSEEVENTF_RIGHTUP = 0x0010
|
||||
MOUSEEVENTF_MIDDLEDOWN = 0x0020
|
||||
MOUSEEVENTF_MIDDLEUP = 0x0040
|
||||
|
||||
# 键盘事件标志
|
||||
KEYEVENTF_KEYUP = 0x0002
|
||||
|
||||
|
||||
class MOUSEINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dx", wintypes.LONG),
|
||||
("dy", wintypes.LONG),
|
||||
("mouseData", wintypes.DWORD),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("time", wintypes.DWORD),
|
||||
("dwExtraInfo", ctypes.POINTER(wintypes.ULONG)),
|
||||
]
|
||||
|
||||
|
||||
class KEYBDINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("wVk", wintypes.WORD),
|
||||
("wScan", wintypes.WORD),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("time", wintypes.DWORD),
|
||||
("dwExtraInfo", ctypes.POINTER(wintypes.ULONG)),
|
||||
]
|
||||
|
||||
|
||||
class INPUT_I(ctypes.Union):
|
||||
_fields_ = [
|
||||
("mi", MOUSEINPUT),
|
||||
("ki", KEYBDINPUT),
|
||||
]
|
||||
|
||||
|
||||
class INPUT(ctypes.Structure):
|
||||
_anonymous_ = ("_input",)
|
||||
_fields_ = [
|
||||
("type", wintypes.DWORD),
|
||||
("_input", INPUT_I),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
"""坐标点"""
|
||||
x: int
|
||||
y: int
|
||||
|
||||
def __add__(self, other):
|
||||
return Point(self.x + other.x, self.y + other.y)
|
||||
|
||||
def __iter__(self):
|
||||
yield self.x
|
||||
yield self.y
|
||||
|
||||
|
||||
class InputSimulator:
|
||||
"""
|
||||
输入模拟器 - 使用 SendInput 模拟真实硬件输入
|
||||
|
||||
对外暴露的简洁接口:
|
||||
- click(x, y, duration=0) -> 点击/长按
|
||||
- swipe(start_x, start_y, end_x, end_y, duration=0.5) -> 滑动
|
||||
- key_press(key) -> 按键
|
||||
- key_down(key) / key_up(key) -> 按住/释放按键
|
||||
"""
|
||||
|
||||
def __init__(self, window_manager: GameWindowManager):
|
||||
self.window = window_manager
|
||||
self._screen_width = ctypes.windll.user32.GetSystemMetrics(0)
|
||||
self._screen_height = ctypes.windll.user32.GetSystemMetrics(1)
|
||||
logger.info(f"InputSimulator 初始化完成,屏幕分辨率: {self._screen_width}x{self._screen_height}")
|
||||
|
||||
def _to_screen_coords(self, x: int, y: int) -> Tuple[int, int]:
|
||||
"""将窗口相对坐标转换为屏幕坐标"""
|
||||
logger.debug(f"[坐标转换] 开始转换窗口相对坐标 ({x}, {y})")
|
||||
|
||||
if not self.window.is_window_captured:
|
||||
logger.error("[坐标转换] 失败: 窗口未捕获")
|
||||
raise RuntimeError("窗口未捕获,无法执行操作")
|
||||
|
||||
import win32gui
|
||||
hwnd = self.window.hwnd
|
||||
|
||||
# 获取窗口客户区在屏幕上的位置
|
||||
client_rect = win32gui.GetClientRect(hwnd)
|
||||
client_left, client_top = win32gui.ClientToScreen(hwnd, (client_rect[0], client_rect[1]))
|
||||
logger.debug(f"[坐标转换] 窗口客户区左上角屏幕坐标: ({client_left}, {client_top})")
|
||||
|
||||
# 计算屏幕坐标
|
||||
screen_x = client_left + x
|
||||
screen_y = client_top + y
|
||||
logger.debug(f"[坐标转换] 计算后的屏幕坐标: ({screen_x}, {screen_y})")
|
||||
|
||||
# 确保在屏幕范围内
|
||||
orig_screen_x, orig_screen_y = screen_x, screen_y
|
||||
screen_x = max(0, min(screen_x, self._screen_width - 1))
|
||||
screen_y = max(0, min(screen_y, self._screen_height - 1))
|
||||
|
||||
if (screen_x, screen_y) != (orig_screen_x, orig_screen_y):
|
||||
logger.warning(f"[坐标转换] 坐标被限制在屏幕范围内: ({orig_screen_x}, {orig_screen_y}) -> ({screen_x}, {screen_y})")
|
||||
|
||||
logger.debug(f"[坐标转换] 完成: 窗口相对 ({x}, {y}) -> 屏幕 ({screen_x}, {screen_y})")
|
||||
return screen_x, screen_y
|
||||
|
||||
def _activate_window(self):
|
||||
"""激活窗口"""
|
||||
logger.debug("[窗口激活] 开始激活窗口")
|
||||
|
||||
if not self.window.is_window_captured:
|
||||
logger.error("[窗口激活] 失败: 窗口未捕获")
|
||||
return False
|
||||
|
||||
import win32gui
|
||||
import win32con
|
||||
|
||||
hwnd = self.window.hwnd
|
||||
logger.debug(f"[窗口激活] 窗口句柄: {hwnd}")
|
||||
|
||||
# 如果窗口最小化,恢复它
|
||||
if win32gui.IsIconic(hwnd):
|
||||
logger.debug("[窗口激活] 窗口已最小化,执行恢复操作")
|
||||
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
|
||||
logger.info("[窗口激活] 窗口已从最小化恢复")
|
||||
|
||||
# 置顶窗口
|
||||
logger.debug("[窗口激活] 设置窗口为前台窗口")
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
|
||||
# 验证窗口是否在前台
|
||||
foreground_hwnd = win32gui.GetForegroundWindow()
|
||||
if foreground_hwnd == hwnd:
|
||||
logger.info("[窗口激活] 窗口激活成功")
|
||||
else:
|
||||
logger.warning(f"[窗口激活] 窗口可能未成功激活,前台窗口句柄: {foreground_hwnd}")
|
||||
|
||||
return True
|
||||
|
||||
def _send_input(self, inputs):
|
||||
"""发送输入事件"""
|
||||
nInputs = len(inputs)
|
||||
LPINPUT = INPUT * nInputs
|
||||
pInputs = LPINPUT(*inputs)
|
||||
cbSize = ctypes.sizeof(INPUT)
|
||||
|
||||
logger.debug(f"[SendInput] 准备发送 {nInputs} 个输入事件,结构体大小: {cbSize}")
|
||||
|
||||
result = ctypes.windll.user32.SendInput(nInputs, pInputs, cbSize)
|
||||
|
||||
if result == 0:
|
||||
error = ctypes.get_last_error()
|
||||
logger.error(f"[SendInput] 发送失败,错误码: {error}")
|
||||
else:
|
||||
logger.debug(f"[SendInput] 发送成功,成功发送 {result} 个事件")
|
||||
|
||||
return result
|
||||
|
||||
def _get_system_time(self) -> int:
|
||||
"""获取系统时间(毫秒)"""
|
||||
return ctypes.windll.kernel32.GetTickCount()
|
||||
|
||||
def _move_mouse_absolute(self, x: int, y: int):
|
||||
"""移动鼠标到绝对坐标(屏幕坐标)"""
|
||||
logger.debug(f"[鼠标移动] 开始移动到屏幕坐标 ({x}, {y})")
|
||||
|
||||
abs_x = int(x * 65535 / (self._screen_width - 1))
|
||||
abs_y = int(y * 65535 / (self._screen_height - 1))
|
||||
logger.debug(f"[鼠标移动] 转换为 SendInput 绝对坐标: ({abs_x}, {abs_y})")
|
||||
|
||||
timestamp = self._get_system_time()
|
||||
|
||||
input_event = INPUT()
|
||||
input_event.type = INPUT_MOUSE
|
||||
input_event.mi = MOUSEINPUT(
|
||||
dx=abs_x,
|
||||
dy=abs_y,
|
||||
mouseData=0,
|
||||
dwFlags=MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE,
|
||||
time=timestamp,
|
||||
dwExtraInfo=None
|
||||
)
|
||||
|
||||
logger.debug(f"[鼠标移动] 发送 MOUSEINPUT 事件: dx={abs_x}, dy={abs_y}, flags=MOUSEEVENTF_ABSOLUTE|MOUSEEVENTF_MOVE")
|
||||
result = self._send_input([input_event])
|
||||
|
||||
if result > 0:
|
||||
logger.info(f"[鼠标移动] 成功移动到屏幕坐标 ({x}, {y})")
|
||||
else:
|
||||
logger.error(f"[鼠标移动] 移动到屏幕坐标 ({x}, {y}) 失败")
|
||||
|
||||
def _mouse_down(self, button: str = "left"):
|
||||
"""鼠标按下"""
|
||||
logger.debug(f"[鼠标按下] 开始按下 {button} 键")
|
||||
|
||||
if button == "left":
|
||||
flags = MOUSEEVENTF_LEFTDOWN
|
||||
button_name = "左键"
|
||||
elif button == "right":
|
||||
flags = MOUSEEVENTF_RIGHTDOWN
|
||||
button_name = "右键"
|
||||
elif button == "middle":
|
||||
flags = MOUSEEVENTF_MIDDLEDOWN
|
||||
button_name = "中键"
|
||||
else:
|
||||
logger.error(f"[鼠标按下] 不支持的按钮类型: {button}")
|
||||
return
|
||||
|
||||
timestamp = self._get_system_time()
|
||||
|
||||
input_event = INPUT()
|
||||
input_event.type = INPUT_MOUSE
|
||||
input_event.mi = MOUSEINPUT(
|
||||
dx=0,
|
||||
dy=0,
|
||||
mouseData=0,
|
||||
dwFlags=flags,
|
||||
time=timestamp,
|
||||
dwExtraInfo=None
|
||||
)
|
||||
|
||||
logger.debug(f"[鼠标按下] 发送 MOUSEINPUT 事件: flags={flags}")
|
||||
result = self._send_input([input_event])
|
||||
|
||||
if result > 0:
|
||||
logger.info(f"[鼠标按下] {button_name} 按下成功")
|
||||
else:
|
||||
logger.error(f"[鼠标按下] {button_name} 按下失败")
|
||||
|
||||
def _mouse_up(self, button: str = "left"):
|
||||
"""鼠标释放"""
|
||||
logger.debug(f"[鼠标释放] 开始释放 {button} 键")
|
||||
|
||||
if button == "left":
|
||||
flags = MOUSEEVENTF_LEFTUP
|
||||
button_name = "左键"
|
||||
elif button == "right":
|
||||
flags = MOUSEEVENTF_RIGHTUP
|
||||
button_name = "右键"
|
||||
elif button == "middle":
|
||||
flags = MOUSEEVENTF_MIDDLEUP
|
||||
button_name = "中键"
|
||||
else:
|
||||
logger.error(f"[鼠标释放] 不支持的按钮类型: {button}")
|
||||
return
|
||||
|
||||
timestamp = self._get_system_time()
|
||||
|
||||
input_event = INPUT()
|
||||
input_event.type = INPUT_MOUSE
|
||||
input_event.mi = MOUSEINPUT(
|
||||
dx=0,
|
||||
dy=0,
|
||||
mouseData=0,
|
||||
dwFlags=flags,
|
||||
time=timestamp,
|
||||
dwExtraInfo=None
|
||||
)
|
||||
|
||||
logger.debug(f"[鼠标释放] 发送 MOUSEINPUT 事件: flags={flags}")
|
||||
result = self._send_input([input_event])
|
||||
|
||||
if result > 0:
|
||||
logger.info(f"[鼠标释放] {button_name} 释放成功")
|
||||
else:
|
||||
logger.error(f"[鼠标释放] {button_name} 释放失败")
|
||||
|
||||
def _key_down(self, vk_code: int):
|
||||
"""按键按下"""
|
||||
logger.debug(f"[按键按下] 开始按下按键,虚拟键码: {vk_code}")
|
||||
|
||||
input_event = INPUT()
|
||||
input_event.type = INPUT_KEYBOARD
|
||||
input_event.ki = KEYBDINPUT(
|
||||
wVk=vk_code,
|
||||
wScan=0,
|
||||
dwFlags=0,
|
||||
time=0,
|
||||
dwExtraInfo=None
|
||||
)
|
||||
|
||||
logger.debug(f"[按键按下] 发送 KEYBDINPUT 事件: wVk={vk_code}")
|
||||
result = self._send_input([input_event])
|
||||
|
||||
if result > 0:
|
||||
logger.info(f"[按键按下] 按键按下成功,虚拟键码: {vk_code}")
|
||||
else:
|
||||
logger.error(f"[按键按下] 按键按下失败,虚拟键码: {vk_code}")
|
||||
|
||||
def _key_up(self, vk_code: int):
|
||||
"""按键释放"""
|
||||
logger.debug(f"[按键释放] 开始释放按键,虚拟键码: {vk_code}")
|
||||
|
||||
input_event = INPUT()
|
||||
input_event.type = INPUT_KEYBOARD
|
||||
input_event.ki = KEYBDINPUT(
|
||||
wVk=vk_code,
|
||||
wScan=0,
|
||||
dwFlags=KEYEVENTF_KEYUP,
|
||||
time=0,
|
||||
dwExtraInfo=None
|
||||
)
|
||||
|
||||
logger.debug(f"[按键释放] 发送 KEYBDINPUT 事件: wVk={vk_code}, flags=KEYEVENTF_KEYUP")
|
||||
result = self._send_input([input_event])
|
||||
|
||||
if result > 0:
|
||||
logger.info(f"[按键释放] 按键释放成功,虚拟键码: {vk_code}")
|
||||
else:
|
||||
logger.error(f"[按键释放] 按键释放失败,虚拟键码: {vk_code}")
|
||||
|
||||
# ==================== 对外接口 ====================
|
||||
|
||||
def _human_like_delay(self, base_delay: float = 0.05):
|
||||
"""模拟人类操作的随机延迟"""
|
||||
delay = base_delay + random.uniform(0.01, 0.03)
|
||||
logger.debug(f"[延迟] 人类模拟延迟: {delay:.3f}s (基础: {base_delay}s)")
|
||||
time.sleep(delay)
|
||||
|
||||
def _add_jitter(self, x: int, y: int, max_jitter: int = 3) -> Tuple[int, int]:
|
||||
"""添加随机抖动,使坐标不完全精确"""
|
||||
jitter_x = random.randint(-max_jitter, max_jitter)
|
||||
jitter_y = random.randint(-max_jitter, max_jitter)
|
||||
new_x, new_y = x + jitter_x, y + jitter_y
|
||||
|
||||
if jitter_x != 0 or jitter_y != 0:
|
||||
logger.debug(f"[抖动] 添加随机抖动: ({x}, {y}) -> ({new_x}, {new_y}), 抖动量: ({jitter_x}, {jitter_y})")
|
||||
|
||||
return new_x, new_y
|
||||
|
||||
def click(self, x: int, y: int, duration: float = 0, button: str = "left", human_like: bool = True):
|
||||
"""
|
||||
点击指定位置
|
||||
|
||||
Args:
|
||||
x, y: 窗口内的相对坐标
|
||||
duration: 按住时长(秒),0表示短按,>0表示长按
|
||||
button: 鼠标按钮,"left"/"right"/"middle"
|
||||
human_like: 是否模拟人类操作(随机延迟和抖动)
|
||||
"""
|
||||
action = "长按" if duration > 0 else "点击"
|
||||
logger.info(f"[点击操作] 开始{action}: 窗口相对坐标 ({x}, {y}), 按钮: {button}, 时长: {duration}s, 人类模拟: {human_like}")
|
||||
|
||||
# 激活窗口
|
||||
logger.debug("[点击操作] 步骤 1/5: 激活窗口")
|
||||
self._activate_window()
|
||||
self._human_like_delay(0.1)
|
||||
|
||||
# 转换为屏幕坐标
|
||||
logger.debug("[点击操作] 步骤 2/5: 坐标转换")
|
||||
screen_x, screen_y = self._to_screen_coords(x, y)
|
||||
|
||||
# 添加人为抖动
|
||||
if human_like:
|
||||
logger.debug("[点击操作] 步骤 3/5: 添加随机抖动")
|
||||
screen_x, screen_y = self._add_jitter(screen_x, screen_y)
|
||||
else:
|
||||
logger.debug("[点击操作] 步骤 3/5: 跳过随机抖动")
|
||||
|
||||
# 使用 SendInput 移动鼠标到目标位置
|
||||
logger.debug("[点击操作] 步骤 4/5: 移动鼠标到目标位置")
|
||||
self._move_mouse_absolute(screen_x, screen_y)
|
||||
self._human_like_delay(0.05)
|
||||
|
||||
# 按下
|
||||
logger.debug(f"[点击操作] 步骤 5/5: 执行{action}")
|
||||
self._mouse_down(button)
|
||||
|
||||
if duration > 0:
|
||||
logger.debug(f"[点击操作] 长按等待: {duration}s")
|
||||
time.sleep(duration)
|
||||
else:
|
||||
self._human_like_delay(0.05)
|
||||
|
||||
# 释放
|
||||
logger.debug("[点击操作] 释放鼠标按钮")
|
||||
self._mouse_up(button)
|
||||
|
||||
logger.info(f"[点击操作] {action}完成: ({x}, {y}) 按钮: {button}, 时长: {duration}s")
|
||||
|
||||
def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int,
|
||||
duration: float = 0.5, button: str = "left"):
|
||||
"""
|
||||
从起点滑动到终点
|
||||
|
||||
Args:
|
||||
start_x, start_y: 起点坐标(窗口内相对坐标)
|
||||
end_x, end_y: 终点坐标(窗口内相对坐标)
|
||||
duration: 滑动持续时间(秒)
|
||||
button: 使用的鼠标按钮
|
||||
"""
|
||||
logger.info(f"[滑动操作] 开始滑动: ({start_x}, {start_y}) -> ({end_x}, {end_y}), 时长: {duration}s, 按钮: {button}")
|
||||
|
||||
# 激活窗口
|
||||
logger.debug("[滑动操作] 步骤 1/4: 激活窗口")
|
||||
self._activate_window()
|
||||
time.sleep(0.1)
|
||||
|
||||
# 转换坐标
|
||||
logger.debug("[滑动操作] 步骤 2/4: 转换起点和终点坐标为屏幕坐标")
|
||||
start_screen = self._to_screen_coords(start_x, start_y)
|
||||
end_screen = self._to_screen_coords(end_x, end_y)
|
||||
logger.info(f"[滑动操作] 屏幕坐标: ({start_screen[0]}, {start_screen[1]}) -> ({end_screen[0]}, {end_screen[1]})")
|
||||
|
||||
# 计算步进
|
||||
steps = max(int(duration * 60), 10)
|
||||
step_delay = duration / steps
|
||||
logger.debug(f"[滑动操作] 滑动参数: 总步数={steps}, 每步延迟={step_delay:.4f}s")
|
||||
|
||||
dx = (end_screen[0] - start_screen[0]) / steps
|
||||
dy = (end_screen[1] - start_screen[1]) / steps
|
||||
logger.debug(f"[滑动操作] 每步移动量: dx={dx:.2f}, dy={dy:.2f}")
|
||||
|
||||
# 移动到起点并按下
|
||||
logger.debug("[滑动操作] 步骤 3/4: 移动到起点并按下鼠标")
|
||||
self._move_mouse_absolute(start_screen[0], start_screen[1])
|
||||
time.sleep(0.05)
|
||||
self._mouse_down(button)
|
||||
time.sleep(0.05)
|
||||
|
||||
# 滑动过程
|
||||
logger.debug("[滑动操作] 开始滑动过程")
|
||||
for i in range(1, steps + 1):
|
||||
new_x = int(start_screen[0] + dx * i)
|
||||
new_y = int(start_screen[1] + dy * i)
|
||||
self._move_mouse_absolute(new_x, new_y)
|
||||
time.sleep(step_delay)
|
||||
|
||||
# 确保到达终点
|
||||
logger.debug("[滑动操作] 步骤 4/4: 确保到达终点并释放鼠标")
|
||||
self._move_mouse_absolute(end_screen[0], end_screen[1])
|
||||
time.sleep(0.05)
|
||||
|
||||
# 释放
|
||||
self._mouse_up(button)
|
||||
|
||||
logger.info(f"[滑动操作] 滑动完成: ({start_x}, {start_y}) -> ({end_x}, {end_y}), 时长: {duration}s")
|
||||
|
||||
def key_press(self, key: str, duration: float = 0.05):
|
||||
"""
|
||||
按下并释放按键
|
||||
|
||||
Args:
|
||||
key: 按键名称,如 'a', 'enter', 'esc', 'space' 等
|
||||
duration: 按住时长(秒)
|
||||
"""
|
||||
logger.info(f"[按键操作] 开始按键: '{key}', 按住时长: {duration}s")
|
||||
|
||||
vk_code = self._get_vk_code(key)
|
||||
if vk_code is None:
|
||||
logger.error(f"[按键操作] 未知的按键: '{key}'")
|
||||
return
|
||||
|
||||
logger.debug(f"[按键操作] 按键 '{key}' 对应的虚拟键码: {vk_code}")
|
||||
|
||||
# 激活窗口
|
||||
logger.debug("[按键操作] 步骤 1/3: 激活窗口")
|
||||
self._activate_window()
|
||||
time.sleep(0.05)
|
||||
|
||||
# 按下
|
||||
logger.debug("[按键操作] 步骤 2/3: 按下按键")
|
||||
self._key_down(vk_code)
|
||||
time.sleep(duration)
|
||||
|
||||
# 释放
|
||||
logger.debug("[按键操作] 步骤 3/3: 释放按键")
|
||||
self._key_up(vk_code)
|
||||
|
||||
logger.info(f"[按键操作] 按键完成: '{key}', 按住时长: {duration}s")
|
||||
|
||||
def key_down(self, key: str):
|
||||
"""按住按键(不释放)"""
|
||||
logger.info(f"[按键按住] 开始按住按键: '{key}'")
|
||||
|
||||
vk_code = self._get_vk_code(key)
|
||||
if vk_code is None:
|
||||
logger.error(f"[按键按住] 未知的按键: '{key}'")
|
||||
return
|
||||
|
||||
logger.debug(f"[按键按住] 按键 '{key}' 对应的虚拟键码: {vk_code}")
|
||||
|
||||
self._activate_window()
|
||||
self._key_down(vk_code)
|
||||
|
||||
logger.info(f"[按键按住] 按键 '{key}' 已按下(未释放)")
|
||||
|
||||
def key_up(self, key: str):
|
||||
"""释放按键"""
|
||||
logger.info(f"[按键释放] 开始释放按键: '{key}'")
|
||||
|
||||
vk_code = self._get_vk_code(key)
|
||||
if vk_code is None:
|
||||
logger.error(f"[按键释放] 未知的按键: '{key}'")
|
||||
return
|
||||
|
||||
logger.debug(f"[按键释放] 按键 '{key}' 对应的虚拟键码: {vk_code}")
|
||||
|
||||
self._key_up(vk_code)
|
||||
|
||||
logger.info(f"[按键释放] 按键 '{key}' 已释放")
|
||||
|
||||
def _get_vk_code(self, key: str) -> Optional[int]:
|
||||
"""获取按键的虚拟键码"""
|
||||
import win32con
|
||||
|
||||
key = key.lower()
|
||||
|
||||
# 字母数字
|
||||
if len(key) == 1 and key.isalnum():
|
||||
vk_code = ord(key.upper())
|
||||
logger.debug(f"[虚拟键码] 字母/数字 '{key}' -> {vk_code}")
|
||||
return vk_code
|
||||
|
||||
# 特殊按键映射
|
||||
special_keys = {
|
||||
'enter': win32con.VK_RETURN,
|
||||
'return': win32con.VK_RETURN,
|
||||
'esc': win32con.VK_ESCAPE,
|
||||
'escape': win32con.VK_ESCAPE,
|
||||
'space': win32con.VK_SPACE,
|
||||
'tab': win32con.VK_TAB,
|
||||
'backspace': win32con.VK_BACK,
|
||||
'delete': win32con.VK_DELETE,
|
||||
'del': win32con.VK_DELETE,
|
||||
'insert': win32con.VK_INSERT,
|
||||
'ins': win32con.VK_INSERT,
|
||||
'home': win32con.VK_HOME,
|
||||
'end': win32con.VK_END,
|
||||
'pageup': win32con.VK_PRIOR,
|
||||
'pagedown': win32con.VK_NEXT,
|
||||
'up': win32con.VK_UP,
|
||||
'down': win32con.VK_DOWN,
|
||||
'left': win32con.VK_LEFT,
|
||||
'right': win32con.VK_RIGHT,
|
||||
'f1': win32con.VK_F1,
|
||||
'f2': win32con.VK_F2,
|
||||
'f3': win32con.VK_F3,
|
||||
'f4': win32con.VK_F4,
|
||||
'f5': win32con.VK_F5,
|
||||
'f6': win32con.VK_F6,
|
||||
'f7': win32con.VK_F7,
|
||||
'f8': win32con.VK_F8,
|
||||
'f9': win32con.VK_F9,
|
||||
'f10': win32con.VK_F10,
|
||||
'f11': win32con.VK_F11,
|
||||
'f12': win32con.VK_F12,
|
||||
'ctrl': win32con.VK_CONTROL,
|
||||
'control': win32con.VK_CONTROL,
|
||||
'shift': win32con.VK_SHIFT,
|
||||
'alt': win32con.VK_MENU,
|
||||
'win': win32con.VK_LWIN,
|
||||
'windows': win32con.VK_LWIN,
|
||||
}
|
||||
|
||||
vk_code = special_keys.get(key)
|
||||
if vk_code:
|
||||
logger.debug(f"[虚拟键码] 特殊键 '{key}' -> {vk_code}")
|
||||
else:
|
||||
logger.warning(f"[虚拟键码] 未找到 '{key}' 对应的虚拟键码")
|
||||
|
||||
return vk_code
|
||||
|
||||
# ==================== 便捷方法 ====================
|
||||
|
||||
def double_click(self, x: int, y: int, button: str = "left"):
|
||||
"""双击"""
|
||||
logger.info(f"[双击操作] 开始在 ({x}, {y}) 双击")
|
||||
|
||||
self.click(x, y, duration=0, button=button)
|
||||
logger.debug("[双击操作] 第一次点击完成,等待 0.1s")
|
||||
time.sleep(0.1)
|
||||
self.click(x, y, duration=0, button=button)
|
||||
|
||||
logger.info(f"[双击操作] 双击完成: ({x}, {y})")
|
||||
|
||||
def long_press(self, x: int, y: int, duration: float = 1.0, button: str = "left"):
|
||||
"""长按(click的别名,更清晰)"""
|
||||
logger.info(f"[长按操作] 开始在 ({x}, {y}) 长按 {duration}s")
|
||||
self.click(x, y, duration=duration, button=button)
|
||||
logger.info(f"[长按操作] 长按完成: ({x}, {y}), 时长: {duration}s")
|
||||
|
||||
def drag(self, start_x: int, start_y: int, end_x: int, end_y: int,
|
||||
duration: float = 0.5, button: str = "left"):
|
||||
"""拖拽(swipe的别名,更清晰)"""
|
||||
logger.info(f"[拖拽操作] 开始拖拽: ({start_x}, {start_y}) -> ({end_x}, {end_y}), 时长: {duration}s")
|
||||
self.swipe(start_x, start_y, end_x, end_y, duration, button)
|
||||
logger.info(f"[拖拽操作] 拖拽完成: ({start_x}, {start_y}) -> ({end_x}, {end_y})")
|
||||
14
src/core/tasks/__init__.py
Normal file
14
src/core/tasks/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
挂机任务模块
|
||||
所有任务逻辑代码写死实现
|
||||
"""
|
||||
|
||||
from .daily_tasks import DailyTaskRunner
|
||||
from .misc_tasks import MiscTaskRunner
|
||||
from .pending_tasks import PendingTaskRunner
|
||||
|
||||
__all__ = [
|
||||
"DailyTaskRunner",
|
||||
"MiscTaskRunner",
|
||||
"PendingTaskRunner",
|
||||
]
|
||||
69
src/core/tasks/daily_tasks.py
Normal file
69
src/core/tasks/daily_tasks.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
日常挂机任务
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from src.utils.logger import logger
|
||||
|
||||
|
||||
class DailyTaskRunner:
|
||||
"""日常任务执行器"""
|
||||
|
||||
# 任务配置 - 代码写死
|
||||
TASKS = {
|
||||
"daily_mission": {
|
||||
"name": "每日委托",
|
||||
"enabled": True,
|
||||
"description": "完成每日任务",
|
||||
},
|
||||
"resin_farming": {
|
||||
"name": "清体力",
|
||||
"enabled": True,
|
||||
"description": "消耗体力刷资源",
|
||||
},
|
||||
"monthly_card": {
|
||||
"name": "领月卡",
|
||||
"enabled": False,
|
||||
"description": "领取月卡奖励",
|
||||
},
|
||||
"friend_gift": {
|
||||
"name": "好友礼物",
|
||||
"enabled": True,
|
||||
"description": "领取好友赠送的礼物",
|
||||
},
|
||||
"shop_daily": {
|
||||
"name": "每日商店",
|
||||
"enabled": False,
|
||||
"description": "购买每日商店物品",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._config = self.TASKS.copy()
|
||||
|
||||
def get_tasks(self) -> Dict[str, Any]:
|
||||
"""获取所有任务配置"""
|
||||
return self._config
|
||||
|
||||
def update_task(self, task_id: str, enabled: bool):
|
||||
"""更新任务启用状态"""
|
||||
if task_id in self._config:
|
||||
self._config[task_id]["enabled"] = enabled
|
||||
logger.info(f"任务 {task_id} 状态更新为: {enabled}")
|
||||
|
||||
def run(self, actions):
|
||||
"""执行启用的任务"""
|
||||
for task_id, config in self._config.items():
|
||||
if not config["enabled"]:
|
||||
continue
|
||||
|
||||
logger.info(f"执行任务: {config['name']}")
|
||||
try:
|
||||
self._execute_task(task_id, actions)
|
||||
except Exception as e:
|
||||
logger.error(f"任务 {config['name']} 执行失败: {e}")
|
||||
|
||||
def _execute_task(self, task_id: str, actions):
|
||||
"""执行具体任务"""
|
||||
# TODO: 实现具体任务逻辑
|
||||
logger.debug(f"执行任务逻辑: {task_id}")
|
||||
64
src/core/tasks/misc_tasks.py
Normal file
64
src/core/tasks/misc_tasks.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
杂项功能任务
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from src.utils.logger import logger
|
||||
|
||||
|
||||
class MiscTaskRunner:
|
||||
"""杂项功能执行器"""
|
||||
|
||||
# 功能配置 - 代码写死
|
||||
FEATURES = {
|
||||
"auto_pickup": {
|
||||
"name": "自动拾取",
|
||||
"enabled": True,
|
||||
"description": "自动拾取掉落物品",
|
||||
},
|
||||
"auto_skip": {
|
||||
"name": "自动跳过对话",
|
||||
"enabled": False,
|
||||
"description": "自动跳过游戏对话",
|
||||
},
|
||||
"auto_heal": {
|
||||
"name": "自动回血",
|
||||
"enabled": True,
|
||||
"description": "低血量自动回血",
|
||||
},
|
||||
"auto_repair": {
|
||||
"name": "自动修理",
|
||||
"enabled": False,
|
||||
"description": "装备损坏自动修理",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._config = self.FEATURES.copy()
|
||||
|
||||
def get_features(self) -> Dict[str, Any]:
|
||||
"""获取所有功能配置"""
|
||||
return self._config
|
||||
|
||||
def update_feature(self, feature_id: str, enabled: bool):
|
||||
"""更新功能启用状态"""
|
||||
if feature_id in self._config:
|
||||
self._config[feature_id]["enabled"] = enabled
|
||||
logger.info(f"功能 {feature_id} 状态更新为: {enabled}")
|
||||
|
||||
def run(self, actions):
|
||||
"""执行启用的功能"""
|
||||
for feature_id, config in self._config.items():
|
||||
if not config["enabled"]:
|
||||
continue
|
||||
|
||||
logger.info(f"执行功能: {config['name']}")
|
||||
try:
|
||||
self._execute_feature(feature_id, actions)
|
||||
except Exception as e:
|
||||
logger.error(f"功能 {config['name']} 执行失败: {e}")
|
||||
|
||||
def _execute_feature(self, feature_id: str, actions):
|
||||
"""执行具体功能"""
|
||||
# TODO: 实现具体功能逻辑
|
||||
logger.debug(f"执行功能逻辑: {feature_id}")
|
||||
48
src/core/tasks/pending_tasks.py
Normal file
48
src/core/tasks/pending_tasks.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
待定功能任务
|
||||
预留功能占位
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from src.utils.logger import logger
|
||||
|
||||
|
||||
class PendingTaskRunner:
|
||||
"""待定功能执行器"""
|
||||
|
||||
# 待定功能配置 - 代码写死
|
||||
PENDING_FEATURES = {
|
||||
"feature_a": {
|
||||
"name": "功能A(待开发)",
|
||||
"enabled": False,
|
||||
"description": "预留功能A",
|
||||
},
|
||||
"feature_b": {
|
||||
"name": "功能B(待开发)",
|
||||
"enabled": False,
|
||||
"description": "预留功能B",
|
||||
},
|
||||
"feature_c": {
|
||||
"name": "功能C(待开发)",
|
||||
"enabled": False,
|
||||
"description": "预留功能C",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._config = self.PENDING_FEATURES.copy()
|
||||
|
||||
def get_features(self) -> Dict[str, Any]:
|
||||
"""获取所有待定功能"""
|
||||
return self._config
|
||||
|
||||
def update_feature(self, feature_id: str, enabled: bool):
|
||||
"""更新功能状态"""
|
||||
if feature_id in self._config:
|
||||
self._config[feature_id]["enabled"] = enabled
|
||||
logger.info(f"待定功能 {feature_id} 状态更新为: {enabled}")
|
||||
|
||||
def run(self, actions):
|
||||
"""执行启用的功能"""
|
||||
logger.info("待定功能模块 - 暂无实现")
|
||||
# 待定功能暂不执行任何操作
|
||||
30
src/main.py
Normal file
30
src/main.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
LuoLuoTool - 《桃源深处有人家》挂机工具
|
||||
作者: moweishan
|
||||
博客: https://bk.moweishan.top/
|
||||
版本: 0.1
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.utils.logger import setup_logger
|
||||
from src.ui import LuoLuoApp
|
||||
|
||||
|
||||
def main():
|
||||
"""程序入口"""
|
||||
# 初始化日志
|
||||
setup_logger()
|
||||
|
||||
# 启动应用
|
||||
app = LuoLuoApp()
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
8
src/ui/__init__.py
Normal file
8
src/ui/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
LuoLuoTool UI模块
|
||||
使用CustomTkinter构建现代化界面
|
||||
"""
|
||||
|
||||
from .app import LuoLuoApp
|
||||
|
||||
__all__ = ["LuoLuoApp"]
|
||||
229
src/ui/app.py
Normal file
229
src/ui/app.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
LuoLuoTool 主应用类
|
||||
窗口标题: LuoLuo
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
from pathlib import Path
|
||||
from src.utils.logger import logger
|
||||
|
||||
from .pages.run_page import RunPage
|
||||
from .pages.daily_config_page import DailyConfigPage
|
||||
from .pages.misc_config_page import MiscConfigPage
|
||||
from .pages.pending_config_page import PendingConfigPage
|
||||
from .pages.about_page import AboutPage
|
||||
|
||||
|
||||
class LuoLuoApp:
|
||||
"""LuoLuoTool主应用"""
|
||||
|
||||
# 窗口配置
|
||||
WINDOW_TITLE = "LuoLuo"
|
||||
WINDOW_WIDTH = 900
|
||||
WINDOW_HEIGHT = 650
|
||||
|
||||
# 侧边栏配置
|
||||
SIDEBAR_WIDTH = 140
|
||||
|
||||
# 主题配置
|
||||
THEME_COLOR = ("#3B8ED0", "#1F6AA5") # 浅色, 深色
|
||||
|
||||
def __init__(self):
|
||||
# 设置主题
|
||||
ctk.set_appearance_mode("System")
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
# 创建主窗口
|
||||
self.root = ctk.CTk()
|
||||
self.root.title(self.WINDOW_TITLE)
|
||||
self.root.geometry(f"{self.WINDOW_WIDTH}x{self.WINDOW_HEIGHT}")
|
||||
self.root.minsize(800, 550)
|
||||
|
||||
# 当前页面
|
||||
self.current_page = None
|
||||
self.pages = {}
|
||||
|
||||
# 初始化UI
|
||||
self._setup_ui()
|
||||
|
||||
# 设置窗口图标(在UI初始化后设置)
|
||||
self._set_window_icon()
|
||||
|
||||
logger.info("LuoLuoApp初始化完成")
|
||||
|
||||
def _set_window_icon(self):
|
||||
"""设置窗口图标(仅左上角)"""
|
||||
assets_dir = Path(__file__).parent.parent.parent / "assets" / "images"
|
||||
ico_path = assets_dir / "luoluoTool.ico"
|
||||
png_path = assets_dir / "luoluoTool.png"
|
||||
|
||||
try:
|
||||
# 优先使用 ICO 文件(Windows 原生支持)
|
||||
if ico_path.exists():
|
||||
self.root.iconbitmap(str(ico_path))
|
||||
logger.info(f"已设置窗口图标(ICO): {ico_path}")
|
||||
elif png_path.exists():
|
||||
# 备用:使用 PNG
|
||||
from PIL import Image, ImageTk
|
||||
icon_image = Image.open(png_path)
|
||||
if icon_image.mode != 'RGBA':
|
||||
icon_image = icon_image.convert('RGBA')
|
||||
icon_image = icon_image.resize((32, 32), Image.Resampling.LANCZOS)
|
||||
self._icon_photo = ImageTk.PhotoImage(icon_image)
|
||||
self.root.iconphoto(True, self._icon_photo)
|
||||
logger.info(f"已设置窗口图标(PNG): {png_path}")
|
||||
else:
|
||||
logger.warning("未找到图标文件")
|
||||
except Exception as e:
|
||||
logger.warning(f"加载图标失败: {e}")
|
||||
|
||||
def _setup_ui(self):
|
||||
"""初始化界面"""
|
||||
# 配置网格布局
|
||||
self.root.grid_columnconfigure(1, weight=1)
|
||||
self.root.grid_rowconfigure(0, weight=1)
|
||||
|
||||
# 创建侧边栏
|
||||
self._create_sidebar()
|
||||
|
||||
# 创建主内容区域
|
||||
self._create_main_content()
|
||||
|
||||
# 创建底部状态栏
|
||||
self._create_statusbar()
|
||||
|
||||
# 初始化页面
|
||||
self._init_pages()
|
||||
|
||||
# 默认显示运行页面
|
||||
self.show_page("run")
|
||||
|
||||
def _create_sidebar(self):
|
||||
"""创建侧边栏"""
|
||||
self.sidebar = ctk.CTkFrame(
|
||||
self.root,
|
||||
width=self.SIDEBAR_WIDTH,
|
||||
corner_radius=0
|
||||
)
|
||||
self.sidebar.grid(row=0, column=0, rowspan=2, sticky="nsew")
|
||||
self.sidebar.grid_rowconfigure(6, weight=1)
|
||||
|
||||
# Logo/标题
|
||||
self.logo_label = ctk.CTkLabel(
|
||||
self.sidebar,
|
||||
text="🎮 LuoLuo",
|
||||
font=ctk.CTkFont(size=18, weight="bold")
|
||||
)
|
||||
self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
|
||||
|
||||
# 侧边栏按钮
|
||||
self.nav_buttons = {}
|
||||
nav_items = [
|
||||
("run", "▶ 运行", 1),
|
||||
("daily", "📋 日常", 2),
|
||||
("misc", "🔧 杂项", 3),
|
||||
("pending", "⏳ 待定", 4),
|
||||
("about", "ℹ 关于", 5),
|
||||
]
|
||||
|
||||
for page_id, text, row in nav_items:
|
||||
btn = ctk.CTkButton(
|
||||
self.sidebar,
|
||||
text=text,
|
||||
anchor="w",
|
||||
fg_color="transparent",
|
||||
text_color=("gray10", "gray90"),
|
||||
hover_color=("gray70", "gray30"),
|
||||
height=40,
|
||||
command=lambda p=page_id: self.show_page(p)
|
||||
)
|
||||
btn.grid(row=row, column=0, padx=10, pady=5, sticky="ew")
|
||||
self.nav_buttons[page_id] = btn
|
||||
|
||||
# 主题切换
|
||||
self.theme_switch = ctk.CTkSwitch(
|
||||
self.sidebar,
|
||||
text="🌙 深色",
|
||||
command=self._toggle_theme
|
||||
)
|
||||
self.theme_switch.grid(row=7, column=0, padx=20, pady=20, sticky="s")
|
||||
|
||||
def _create_main_content(self):
|
||||
"""创建主内容区域"""
|
||||
self.main_frame = ctk.CTkFrame(self.root, corner_radius=0, fg_color="transparent")
|
||||
self.main_frame.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)
|
||||
self.main_frame.grid_columnconfigure(0, weight=1)
|
||||
self.main_frame.grid_rowconfigure(0, weight=1)
|
||||
|
||||
def _create_statusbar(self):
|
||||
"""创建状态栏"""
|
||||
self.statusbar = ctk.CTkFrame(self.root, height=30, corner_radius=0)
|
||||
self.statusbar.grid(row=1, column=1, sticky="ew", padx=10, pady=(0, 5))
|
||||
|
||||
self.status_label = ctk.CTkLabel(
|
||||
self.statusbar,
|
||||
text="就绪",
|
||||
font=ctk.CTkFont(size=12)
|
||||
)
|
||||
self.status_label.pack(side="left", padx=10)
|
||||
|
||||
self.version_label = ctk.CTkLabel(
|
||||
self.statusbar,
|
||||
text="v0.1",
|
||||
font=ctk.CTkFont(size=12),
|
||||
text_color="gray"
|
||||
)
|
||||
self.version_label.pack(side="right", padx=10)
|
||||
|
||||
def _init_pages(self):
|
||||
"""初始化所有页面 - 预加载提升切换速度"""
|
||||
self.pages["run"] = RunPage(self.main_frame, self)
|
||||
self.pages["daily"] = DailyConfigPage(self.main_frame, self)
|
||||
self.pages["misc"] = MiscConfigPage(self.main_frame, self)
|
||||
self.pages["pending"] = PendingConfigPage(self.main_frame, self)
|
||||
self.pages["about"] = AboutPage(self.main_frame, self)
|
||||
|
||||
# 预构建所有页面,避免懒加载导致的卡顿
|
||||
for page_id, page in self.pages.items():
|
||||
if page.frame is None:
|
||||
page.frame = page.build()
|
||||
|
||||
def show_page(self, page_id: str):
|
||||
"""显示指定页面 - 优化切换性能"""
|
||||
if self.current_page == page_id:
|
||||
return
|
||||
|
||||
# 使用 after 延迟更新UI,避免阻塞
|
||||
self.root.after(0, lambda: self._do_show_page(page_id))
|
||||
|
||||
def _do_show_page(self, page_id: str):
|
||||
"""实际执行页面切换"""
|
||||
# 隐藏当前页面
|
||||
if self.current_page:
|
||||
self.pages[self.current_page].hide()
|
||||
self.nav_buttons[self.current_page].configure(fg_color="transparent")
|
||||
|
||||
# 显示新页面
|
||||
self.current_page = page_id
|
||||
self.pages[page_id].show()
|
||||
self.nav_buttons[page_id].configure(fg_color=self.THEME_COLOR)
|
||||
|
||||
logger.debug(f"切换到页面: {page_id}")
|
||||
|
||||
def _toggle_theme(self):
|
||||
"""切换主题"""
|
||||
if self.theme_switch.get():
|
||||
ctk.set_appearance_mode("Dark")
|
||||
self.theme_switch.configure(text="☀ 浅色")
|
||||
else:
|
||||
ctk.set_appearance_mode("Light")
|
||||
self.theme_switch.configure(text="🌙 深色")
|
||||
|
||||
def set_status(self, text: str):
|
||||
"""设置状态栏文本"""
|
||||
self.status_label.configure(text=text)
|
||||
|
||||
def run(self):
|
||||
"""运行应用"""
|
||||
logger.info("启动LuoLuo应用")
|
||||
self.root.mainloop()
|
||||
19
src/ui/pages/__init__.py
Normal file
19
src/ui/pages/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
UI页面模块
|
||||
"""
|
||||
|
||||
from .base_page import BasePage
|
||||
from .run_page import RunPage
|
||||
from .daily_config_page import DailyConfigPage
|
||||
from .misc_config_page import MiscConfigPage
|
||||
from .pending_config_page import PendingConfigPage
|
||||
from .about_page import AboutPage
|
||||
|
||||
__all__ = [
|
||||
"BasePage",
|
||||
"RunPage",
|
||||
"DailyConfigPage",
|
||||
"MiscConfigPage",
|
||||
"PendingConfigPage",
|
||||
"AboutPage",
|
||||
]
|
||||
154
src/ui/pages/about_page.py
Normal file
154
src/ui/pages/about_page.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
关于页面
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
from .base_page import BasePage
|
||||
|
||||
|
||||
class AboutPage(BasePage):
|
||||
"""关于页面"""
|
||||
|
||||
# 项目信息
|
||||
VERSION = "0.1"
|
||||
AUTHOR = "moweishan"
|
||||
BLOG_URL = "https://bk.moweishan.top/"
|
||||
DESCRIPTION = "专为《桃源深处有人家》打造的\n日常任务挂机工具"
|
||||
|
||||
def build(self) -> ctk.CTkFrame:
|
||||
"""构建关于页面"""
|
||||
frame = ctk.CTkFrame(self.parent, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
frame.grid_rowconfigure(0, weight=1)
|
||||
|
||||
# 居中容器
|
||||
center_frame = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
center_frame.grid(row=0, column=0, pady=50)
|
||||
|
||||
# 加载并显示图标
|
||||
self._load_icon(center_frame)
|
||||
|
||||
# 应用名称
|
||||
name_label = ctk.CTkLabel(
|
||||
center_frame,
|
||||
text="LuoLuoTool",
|
||||
font=ctk.CTkFont(size=28, weight="bold")
|
||||
)
|
||||
name_label.pack()
|
||||
|
||||
# 版本号
|
||||
version_label = ctk.CTkLabel(
|
||||
center_frame,
|
||||
text=f"版本 {self.VERSION}",
|
||||
font=ctk.CTkFont(size=14),
|
||||
text_color="gray"
|
||||
)
|
||||
version_label.pack(pady=(5, 20))
|
||||
|
||||
# 分隔线
|
||||
separator = ctk.CTkFrame(center_frame, height=2, width=200)
|
||||
separator.pack(pady=10)
|
||||
|
||||
# 描述
|
||||
desc_label = ctk.CTkLabel(
|
||||
center_frame,
|
||||
text=self.DESCRIPTION,
|
||||
font=ctk.CTkFont(size=13),
|
||||
justify="center"
|
||||
)
|
||||
desc_label.pack(pady=20)
|
||||
|
||||
# 分隔线
|
||||
separator2 = ctk.CTkFrame(center_frame, height=2, width=200)
|
||||
separator2.pack(pady=10)
|
||||
|
||||
# 作者信息
|
||||
author_frame = ctk.CTkFrame(center_frame, fg_color="transparent")
|
||||
author_frame.pack(pady=20)
|
||||
|
||||
author_label = ctk.CTkLabel(
|
||||
author_frame,
|
||||
text=f"作者: {self.AUTHOR}",
|
||||
font=ctk.CTkFont(size=13)
|
||||
)
|
||||
author_label.pack()
|
||||
|
||||
# 博客链接
|
||||
blog_btn = ctk.CTkButton(
|
||||
author_frame,
|
||||
text="🌐 访问博客",
|
||||
command=self._open_blog
|
||||
)
|
||||
blog_btn.pack(pady=10)
|
||||
|
||||
# 按钮区域
|
||||
btn_frame = ctk.CTkFrame(center_frame, fg_color="transparent")
|
||||
btn_frame.pack(pady=20)
|
||||
|
||||
check_update_btn = ctk.CTkButton(
|
||||
btn_frame,
|
||||
text="🔍 检查更新",
|
||||
command=self._check_update
|
||||
)
|
||||
check_update_btn.pack(side="left", padx=5)
|
||||
|
||||
return frame
|
||||
|
||||
def _load_icon(self, parent):
|
||||
"""加载并显示图标"""
|
||||
assets_dir = Path(__file__).parent.parent.parent.parent / "assets" / "images"
|
||||
|
||||
# 优先使用 PNG 格式在关于页面显示
|
||||
png_path = assets_dir / "luoluoTool.png"
|
||||
ico_path = assets_dir / "luoluoTool.ico"
|
||||
|
||||
try:
|
||||
if png_path.exists():
|
||||
# 使用 PNG 图标
|
||||
icon_image = Image.open(png_path)
|
||||
# 调整大小为 100x100
|
||||
icon_image = icon_image.resize((100, 100), Image.Resampling.LANCZOS)
|
||||
icon_ctk = ctk.CTkImage(light_image=icon_image, dark_image=icon_image, size=(100, 100))
|
||||
|
||||
icon_label = ctk.CTkLabel(parent, image=icon_ctk, text="")
|
||||
icon_label.pack(pady=(0, 20))
|
||||
# 保持引用防止GC
|
||||
self._about_icon = icon_ctk
|
||||
elif ico_path.exists():
|
||||
# 如果没有PNG,尝试ICO
|
||||
icon_image = Image.open(ico_path)
|
||||
icon_image = icon_image.resize((100, 100), Image.Resampling.LANCZOS)
|
||||
icon_ctk = ctk.CTkImage(light_image=icon_image, dark_image=icon_image, size=(100, 100))
|
||||
|
||||
icon_label = ctk.CTkLabel(parent, image=icon_ctk, text="")
|
||||
icon_label.pack(pady=(0, 20))
|
||||
self._about_icon = icon_ctk
|
||||
else:
|
||||
# 使用默认emoji
|
||||
icon_label = ctk.CTkLabel(
|
||||
parent,
|
||||
text="🎮",
|
||||
font=ctk.CTkFont(size=64)
|
||||
)
|
||||
icon_label.pack(pady=(0, 20))
|
||||
except Exception as e:
|
||||
# 出错时使用默认emoji
|
||||
icon_label = ctk.CTkLabel(
|
||||
parent,
|
||||
text="🎮",
|
||||
font=ctk.CTkFont(size=64)
|
||||
)
|
||||
icon_label.pack(pady=(0, 20))
|
||||
|
||||
def _open_blog(self):
|
||||
"""打开作者博客"""
|
||||
webbrowser.open(self.BLOG_URL)
|
||||
|
||||
def _check_update(self):
|
||||
"""检查更新"""
|
||||
# TODO: 实现检查更新功能
|
||||
self.app.set_status("当前已是最新版本")
|
||||
40
src/ui/pages/base_page.py
Normal file
40
src/ui/pages/base_page.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
页面基类
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class BasePage:
|
||||
"""页面基类"""
|
||||
|
||||
def __init__(self, parent: ctk.CTkFrame, app: "LuoLuoApp"):
|
||||
self.parent = parent
|
||||
self.app = app
|
||||
self.frame: Optional[ctk.CTkFrame] = None
|
||||
|
||||
def build(self) -> ctk.CTkFrame:
|
||||
"""构建页面,子类重写"""
|
||||
raise NotImplementedError
|
||||
|
||||
def show(self):
|
||||
"""显示页面"""
|
||||
if self.frame is None:
|
||||
self.frame = self.build()
|
||||
self.frame.grid(row=0, column=0, sticky="nsew")
|
||||
self.on_show()
|
||||
|
||||
def hide(self):
|
||||
"""隐藏页面"""
|
||||
if self.frame:
|
||||
self.frame.grid_forget()
|
||||
self.on_hide()
|
||||
|
||||
def on_show(self):
|
||||
"""页面显示时的回调,子类可重写"""
|
||||
pass
|
||||
|
||||
def on_hide(self):
|
||||
"""页面隐藏时的回调,子类可重写"""
|
||||
pass
|
||||
99
src/ui/pages/daily_config_page.py
Normal file
99
src/ui/pages/daily_config_page.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
日常挂机配置页面
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
|
||||
from .base_page import BasePage
|
||||
from ...core.tasks.daily_tasks import DailyTaskRunner
|
||||
|
||||
|
||||
class DailyConfigPage(BasePage):
|
||||
"""日常挂机配置页面"""
|
||||
|
||||
def __init__(self, parent: ctk.CTkFrame, app: "LuoLuoApp"):
|
||||
super().__init__(parent, app)
|
||||
self.task_runner = DailyTaskRunner()
|
||||
self.switches = {}
|
||||
|
||||
def build(self) -> ctk.CTkFrame:
|
||||
"""构建页面"""
|
||||
frame = ctk.CTkFrame(self.parent, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# 标题
|
||||
title = ctk.CTkLabel(
|
||||
frame,
|
||||
text="📋 日常挂机配置",
|
||||
font=ctk.CTkFont(size=18, weight="bold")
|
||||
)
|
||||
title.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="w")
|
||||
|
||||
# 说明
|
||||
desc = ctk.CTkLabel(
|
||||
frame,
|
||||
text="配置每日自动执行的日常任务",
|
||||
font=ctk.CTkFont(size=12),
|
||||
text_color="gray"
|
||||
)
|
||||
desc.grid(row=1, column=0, padx=20, pady=(0, 20), sticky="w")
|
||||
|
||||
# 任务列表
|
||||
tasks_frame = ctk.CTkFrame(frame)
|
||||
tasks_frame.grid(row=2, column=0, sticky="ew", padx=20, pady=10)
|
||||
tasks_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
tasks = self.task_runner.get_tasks()
|
||||
for idx, (task_id, config) in enumerate(tasks.items()):
|
||||
self._create_task_item(tasks_frame, task_id, config, idx)
|
||||
|
||||
# 保存按钮
|
||||
save_btn = ctk.CTkButton(
|
||||
frame,
|
||||
text="💾 保存配置",
|
||||
command=self._save_config
|
||||
)
|
||||
save_btn.grid(row=3, column=0, padx=20, pady=20, sticky="e")
|
||||
|
||||
return frame
|
||||
|
||||
def _create_task_item(self, parent, task_id: str, config: dict, row: int):
|
||||
"""创建任务项"""
|
||||
frame = ctk.CTkFrame(parent, fg_color="transparent")
|
||||
frame.grid(row=row, column=0, sticky="ew", padx=10, pady=5)
|
||||
frame.grid_columnconfigure(1, weight=1)
|
||||
|
||||
# 开关
|
||||
switch = ctk.CTkSwitch(
|
||||
frame,
|
||||
text="",
|
||||
width=50
|
||||
)
|
||||
switch.grid(row=0, column=0, padx=(0, 10))
|
||||
if config["enabled"]:
|
||||
switch.select()
|
||||
self.switches[task_id] = switch
|
||||
|
||||
# 名称
|
||||
name_label = ctk.CTkLabel(
|
||||
frame,
|
||||
text=config["name"],
|
||||
font=ctk.CTkFont(size=13, weight="bold")
|
||||
)
|
||||
name_label.grid(row=0, column=1, sticky="w")
|
||||
|
||||
# 描述
|
||||
desc_label = ctk.CTkLabel(
|
||||
frame,
|
||||
text=config["description"],
|
||||
font=ctk.CTkFont(size=11),
|
||||
text_color="gray"
|
||||
)
|
||||
desc_label.grid(row=1, column=1, sticky="w")
|
||||
|
||||
def _save_config(self):
|
||||
"""保存配置"""
|
||||
for task_id, switch in self.switches.items():
|
||||
enabled = switch.get() == 1
|
||||
self.task_runner.update_task(task_id, enabled)
|
||||
self.app.set_status("日常配置已保存")
|
||||
99
src/ui/pages/misc_config_page.py
Normal file
99
src/ui/pages/misc_config_page.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
杂项功能配置页面
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
|
||||
from .base_page import BasePage
|
||||
from ...core.tasks.misc_tasks import MiscTaskRunner
|
||||
|
||||
|
||||
class MiscConfigPage(BasePage):
|
||||
"""杂项功能配置页面"""
|
||||
|
||||
def __init__(self, parent: ctk.CTkFrame, app: "LuoLuoApp"):
|
||||
super().__init__(parent, app)
|
||||
self.task_runner = MiscTaskRunner()
|
||||
self.switches = {}
|
||||
|
||||
def build(self) -> ctk.CTkFrame:
|
||||
"""构建页面"""
|
||||
frame = ctk.CTkFrame(self.parent, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# 标题
|
||||
title = ctk.CTkLabel(
|
||||
frame,
|
||||
text="🔧 杂项功能配置",
|
||||
font=ctk.CTkFont(size=18, weight="bold")
|
||||
)
|
||||
title.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="w")
|
||||
|
||||
# 说明
|
||||
desc = ctk.CTkLabel(
|
||||
frame,
|
||||
text="配置挂机时的辅助功能",
|
||||
font=ctk.CTkFont(size=12),
|
||||
text_color="gray"
|
||||
)
|
||||
desc.grid(row=1, column=0, padx=20, pady=(0, 20), sticky="w")
|
||||
|
||||
# 功能列表
|
||||
features_frame = ctk.CTkFrame(frame)
|
||||
features_frame.grid(row=2, column=0, sticky="ew", padx=20, pady=10)
|
||||
features_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
features = self.task_runner.get_features()
|
||||
for idx, (feature_id, config) in enumerate(features.items()):
|
||||
self._create_feature_item(features_frame, feature_id, config, idx)
|
||||
|
||||
# 保存按钮
|
||||
save_btn = ctk.CTkButton(
|
||||
frame,
|
||||
text="💾 保存配置",
|
||||
command=self._save_config
|
||||
)
|
||||
save_btn.grid(row=3, column=0, padx=20, pady=20, sticky="e")
|
||||
|
||||
return frame
|
||||
|
||||
def _create_feature_item(self, parent, feature_id: str, config: dict, row: int):
|
||||
"""创建功能项"""
|
||||
frame = ctk.CTkFrame(parent, fg_color="transparent")
|
||||
frame.grid(row=row, column=0, sticky="ew", padx=10, pady=5)
|
||||
frame.grid_columnconfigure(1, weight=1)
|
||||
|
||||
# 开关
|
||||
switch = ctk.CTkSwitch(
|
||||
frame,
|
||||
text="",
|
||||
width=50
|
||||
)
|
||||
switch.grid(row=0, column=0, padx=(0, 10))
|
||||
if config["enabled"]:
|
||||
switch.select()
|
||||
self.switches[feature_id] = switch
|
||||
|
||||
# 名称
|
||||
name_label = ctk.CTkLabel(
|
||||
frame,
|
||||
text=config["name"],
|
||||
font=ctk.CTkFont(size=13, weight="bold")
|
||||
)
|
||||
name_label.grid(row=0, column=1, sticky="w")
|
||||
|
||||
# 描述
|
||||
desc_label = ctk.CTkLabel(
|
||||
frame,
|
||||
text=config["description"],
|
||||
font=ctk.CTkFont(size=11),
|
||||
text_color="gray"
|
||||
)
|
||||
desc_label.grid(row=1, column=1, sticky="w")
|
||||
|
||||
def _save_config(self):
|
||||
"""保存配置"""
|
||||
for feature_id, switch in self.switches.items():
|
||||
enabled = switch.get() == 1
|
||||
self.task_runner.update_feature(feature_id, enabled)
|
||||
self.app.set_status("杂项配置已保存")
|
||||
96
src/ui/pages/pending_config_page.py
Normal file
96
src/ui/pages/pending_config_page.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
待定功能配置页面
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
|
||||
from .base_page import BasePage
|
||||
from ...core.tasks.pending_tasks import PendingTaskRunner
|
||||
|
||||
|
||||
class PendingConfigPage(BasePage):
|
||||
"""待定功能配置页面"""
|
||||
|
||||
def __init__(self, parent: ctk.CTkFrame, app: "LuoLuoApp"):
|
||||
super().__init__(parent, app)
|
||||
self.task_runner = PendingTaskRunner()
|
||||
self.switches = {}
|
||||
|
||||
def build(self) -> ctk.CTkFrame:
|
||||
"""构建页面"""
|
||||
frame = ctk.CTkFrame(self.parent, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# 标题
|
||||
title = ctk.CTkLabel(
|
||||
frame,
|
||||
text="⏳ 待定功能配置",
|
||||
font=ctk.CTkFont(size=18, weight="bold")
|
||||
)
|
||||
title.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="w")
|
||||
|
||||
# 说明
|
||||
desc = ctk.CTkLabel(
|
||||
frame,
|
||||
text="预留功能配置(待开发)",
|
||||
font=ctk.CTkFont(size=12),
|
||||
text_color="gray"
|
||||
)
|
||||
desc.grid(row=1, column=0, padx=20, pady=(0, 20), sticky="w")
|
||||
|
||||
# 功能列表
|
||||
features_frame = ctk.CTkFrame(frame)
|
||||
features_frame.grid(row=2, column=0, sticky="ew", padx=20, pady=10)
|
||||
features_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
features = self.task_runner.get_features()
|
||||
for idx, (feature_id, config) in enumerate(features.items()):
|
||||
self._create_feature_item(features_frame, feature_id, config, idx)
|
||||
|
||||
# 提示信息
|
||||
info_frame = ctk.CTkFrame(frame, fg_color=("#fff3cd", "#3d3a2a"))
|
||||
info_frame.grid(row=3, column=0, sticky="ew", padx=20, pady=20)
|
||||
|
||||
info_label = ctk.CTkLabel(
|
||||
info_frame,
|
||||
text="💡 这些功能正在开发中,敬请期待!",
|
||||
font=ctk.CTkFont(size=12)
|
||||
)
|
||||
info_label.pack(padx=20, pady=15)
|
||||
|
||||
return frame
|
||||
|
||||
def _create_feature_item(self, parent, feature_id: str, config: dict, row: int):
|
||||
"""创建功能项"""
|
||||
frame = ctk.CTkFrame(parent, fg_color="transparent")
|
||||
frame.grid(row=row, column=0, sticky="ew", padx=10, pady=5)
|
||||
frame.grid_columnconfigure(1, weight=1)
|
||||
|
||||
# 开关(禁用状态)
|
||||
switch = ctk.CTkSwitch(
|
||||
frame,
|
||||
text="",
|
||||
width=50,
|
||||
state="disabled"
|
||||
)
|
||||
switch.grid(row=0, column=0, padx=(0, 10))
|
||||
if config["enabled"]:
|
||||
switch.select()
|
||||
self.switches[feature_id] = switch
|
||||
|
||||
# 名称
|
||||
name_label = ctk.CTkLabel(
|
||||
frame,
|
||||
text=config["name"],
|
||||
font=ctk.CTkFont(size=13, weight="bold")
|
||||
)
|
||||
name_label.grid(row=0, column=1, sticky="w")
|
||||
|
||||
# 描述
|
||||
desc_label = ctk.CTkLabel(
|
||||
frame,
|
||||
text=config["description"],
|
||||
font=ctk.CTkFont(size=11),
|
||||
text_color="gray"
|
||||
)
|
||||
desc_label.grid(row=1, column=1, sticky="w")
|
||||
200
src/ui/pages/run_page.py
Normal file
200
src/ui/pages/run_page.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
运行页面 - 主控制面板
|
||||
包含基础配置和运行控制
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
from src.utils.logger import logger
|
||||
|
||||
from .base_page import BasePage
|
||||
from ...core.automation import AutomationController
|
||||
|
||||
|
||||
class RunPage(BasePage):
|
||||
"""运行页面"""
|
||||
|
||||
def __init__(self, parent: ctk.CTkFrame, app: "LuoLuoApp"):
|
||||
super().__init__(parent, app)
|
||||
self.automation = AutomationController()
|
||||
self.automation.set_callback(self._on_automation_message)
|
||||
|
||||
self.log_textbox: Optional[ctk.CTkTextbox] = None
|
||||
|
||||
def build(self) -> ctk.CTkFrame:
|
||||
"""构建运行页面"""
|
||||
frame = ctk.CTkFrame(self.parent, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
frame.grid_rowconfigure(3, weight=1)
|
||||
|
||||
# ===== 基础配置区域 =====
|
||||
config_frame = ctk.CTkFrame(frame)
|
||||
config_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
||||
config_frame.grid_columnconfigure(1, weight=1)
|
||||
|
||||
ctk.CTkLabel(
|
||||
config_frame,
|
||||
text="⚙️ 基础配置",
|
||||
font=ctk.CTkFont(size=14, weight="bold")
|
||||
).grid(row=0, column=0, columnspan=3, padx=10, pady=(10, 5), sticky="w")
|
||||
|
||||
# 游戏窗口
|
||||
ctk.CTkLabel(config_frame, text="游戏窗口:").grid(row=1, column=0, padx=10, pady=5, sticky="w")
|
||||
self.window_label = ctk.CTkLabel(config_frame, text="未捕获", text_color="gray")
|
||||
self.window_label.grid(row=1, column=1, padx=10, pady=5, sticky="w")
|
||||
self.capture_btn = ctk.CTkButton(
|
||||
config_frame,
|
||||
text="🔍 重新捕获",
|
||||
width=100,
|
||||
command=self._capture_window
|
||||
)
|
||||
self.capture_btn.grid(row=1, column=2, padx=10, pady=5)
|
||||
|
||||
# 运行热键
|
||||
ctk.CTkLabel(config_frame, text="运行热键:").grid(row=2, column=0, padx=10, pady=5, sticky="w")
|
||||
self.hotkey_entry = ctk.CTkEntry(config_frame, placeholder_text="F9")
|
||||
self.hotkey_entry.insert(0, "F9")
|
||||
self.hotkey_entry.grid(row=2, column=1, padx=10, pady=5, sticky="w")
|
||||
|
||||
# 日志等级
|
||||
ctk.CTkLabel(config_frame, text="日志等级:").grid(row=3, column=0, padx=10, pady=5, sticky="w")
|
||||
self.loglevel_combo = ctk.CTkComboBox(
|
||||
config_frame,
|
||||
values=["DEBUG", "INFO", "WARNING", "ERROR"],
|
||||
width=120
|
||||
)
|
||||
self.loglevel_combo.set("INFO")
|
||||
self.loglevel_combo.grid(row=3, column=1, padx=10, pady=5, sticky="w")
|
||||
|
||||
# ===== 窗口状态区域 =====
|
||||
status_frame = ctk.CTkFrame(frame)
|
||||
status_frame.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
|
||||
|
||||
self.status_label = ctk.CTkLabel(
|
||||
status_frame,
|
||||
text="🎮 等待捕获游戏窗口...",
|
||||
font=ctk.CTkFont(size=12)
|
||||
)
|
||||
self.status_label.pack(padx=20, pady=15)
|
||||
|
||||
# ===== 控制按钮区域 =====
|
||||
control_frame = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
control_frame.grid(row=2, column=0, sticky="ew", padx=5, pady=10)
|
||||
|
||||
self.start_btn = ctk.CTkButton(
|
||||
control_frame,
|
||||
text="🚀 开始挂机",
|
||||
font=ctk.CTkFont(size=14, weight="bold"),
|
||||
height=40,
|
||||
fg_color="#2ecc71",
|
||||
hover_color="#27ae60",
|
||||
command=self._start_automation
|
||||
)
|
||||
self.start_btn.pack(side="left", padx=5)
|
||||
|
||||
self.stop_btn = ctk.CTkButton(
|
||||
control_frame,
|
||||
text="⏹ 停止",
|
||||
font=ctk.CTkFont(size=14),
|
||||
height=40,
|
||||
fg_color="#e74c3c",
|
||||
hover_color="#c0392b",
|
||||
state="disabled",
|
||||
command=self._stop_automation
|
||||
)
|
||||
self.stop_btn.pack(side="left", padx=5)
|
||||
|
||||
self.pause_btn = ctk.CTkButton(
|
||||
control_frame,
|
||||
text="⏸ 暂停",
|
||||
font=ctk.CTkFont(size=14),
|
||||
height=40,
|
||||
state="disabled",
|
||||
command=self._pause_automation
|
||||
)
|
||||
self.pause_btn.pack(side="left", padx=5)
|
||||
|
||||
# ===== 日志区域 =====
|
||||
log_frame = ctk.CTkFrame(frame)
|
||||
log_frame.grid(row=3, column=0, sticky="nsew", padx=5, pady=5)
|
||||
log_frame.grid_columnconfigure(0, weight=1)
|
||||
log_frame.grid_rowconfigure(1, weight=1)
|
||||
|
||||
ctk.CTkLabel(
|
||||
log_frame,
|
||||
text="📋 运行日志",
|
||||
font=ctk.CTkFont(size=12, weight="bold")
|
||||
).grid(row=0, column=0, padx=10, pady=(10, 5), sticky="w")
|
||||
|
||||
self.log_textbox = ctk.CTkTextbox(
|
||||
log_frame,
|
||||
wrap="word",
|
||||
state="disabled",
|
||||
font=ctk.CTkFont(size=11)
|
||||
)
|
||||
self.log_textbox.grid(row=1, column=0, sticky="nsew", padx=10, pady=(0, 10))
|
||||
|
||||
# 尝试自动捕获窗口
|
||||
self._capture_window()
|
||||
|
||||
return frame
|
||||
|
||||
def _capture_window(self):
|
||||
"""捕获游戏窗口"""
|
||||
if self.automation.window_manager.capture_window():
|
||||
size = self.automation.window_manager.client_size
|
||||
self.window_label.configure(
|
||||
text=f"桃源深处有人家 - {size[0]}x{size[1]}",
|
||||
text_color="#2ecc71"
|
||||
)
|
||||
self.status_label.configure(
|
||||
text=f"✅ 已捕获游戏窗口 ({size[0]}x{size[1]})",
|
||||
text_color="#2ecc71"
|
||||
)
|
||||
self.app.set_status("窗口已捕获")
|
||||
else:
|
||||
self.window_label.configure(text="未捕获", text_color="#e74c3c")
|
||||
self.status_label.configure(
|
||||
text="❌ 未找到游戏窗口,请确保游戏已运行",
|
||||
text_color="#e74c3c"
|
||||
)
|
||||
self.app.set_status("未找到游戏窗口")
|
||||
|
||||
def _start_automation(self):
|
||||
"""开始自动化"""
|
||||
self.automation.start()
|
||||
self.start_btn.configure(state="disabled")
|
||||
self.stop_btn.configure(state="normal")
|
||||
self.pause_btn.configure(state="normal", text="⏸ 暂停")
|
||||
self.app.set_status("运行中")
|
||||
|
||||
def _stop_automation(self):
|
||||
"""停止自动化"""
|
||||
self.automation.stop()
|
||||
self.start_btn.configure(state="normal")
|
||||
self.stop_btn.configure(state="disabled")
|
||||
self.pause_btn.configure(state="disabled", text="⏸ 暂停")
|
||||
self.app.set_status("已停止")
|
||||
|
||||
def _pause_automation(self):
|
||||
"""暂停/继续"""
|
||||
self.automation.pause()
|
||||
if self.automation.is_paused:
|
||||
self.pause_btn.configure(text="▶ 继续")
|
||||
self.app.set_status("已暂停")
|
||||
else:
|
||||
self.pause_btn.configure(text="⏸ 暂停")
|
||||
self.app.set_status("运行中")
|
||||
|
||||
def _on_automation_message(self, message: str):
|
||||
"""接收自动化消息"""
|
||||
self._add_log(message)
|
||||
|
||||
def _add_log(self, message: str):
|
||||
"""添加日志"""
|
||||
if self.log_textbox:
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
self.log_textbox.configure(state="normal")
|
||||
self.log_textbox.insert("end", f"[{timestamp}] {message}\n")
|
||||
self.log_textbox.see("end")
|
||||
self.log_textbox.configure(state="disabled")
|
||||
8
src/utils/__init__.py
Normal file
8
src/utils/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
工具模块
|
||||
"""
|
||||
|
||||
from .logger import setup_logger
|
||||
from .config import ConfigManager
|
||||
|
||||
__all__ = ["setup_logger", "ConfigManager"]
|
||||
93
src/utils/config.py
Normal file
93
src/utils/config.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
配置管理
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from src.utils.logger import logger
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""配置管理器"""
|
||||
|
||||
# 默认配置
|
||||
DEFAULT_CONFIG = {
|
||||
"version": "0.1",
|
||||
"window": {
|
||||
"width": 900,
|
||||
"height": 650,
|
||||
"theme": "System", # System, Light, Dark
|
||||
},
|
||||
"automation": {
|
||||
"hotkey": "F9",
|
||||
"log_level": "INFO",
|
||||
},
|
||||
"tasks": {
|
||||
"daily": {},
|
||||
"misc": {},
|
||||
"pending": {},
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, config_path: Path = None):
|
||||
if config_path is None:
|
||||
config_path = Path(__file__).parent.parent.parent / "configs" / "settings.json"
|
||||
|
||||
self.config_path = config_path
|
||||
self.config_path.parent.mkdir(exist_ok=True)
|
||||
|
||||
self._config = self.DEFAULT_CONFIG.copy()
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
"""加载配置"""
|
||||
if self.config_path.exists():
|
||||
try:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
loaded = json.load(f)
|
||||
self._config.update(loaded)
|
||||
logger.info(f"配置已加载: {self.config_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"加载配置失败: {e}")
|
||||
self.save() # 保存默认配置
|
||||
else:
|
||||
self.save() # 首次运行,保存默认配置
|
||||
|
||||
def save(self):
|
||||
"""保存配置"""
|
||||
try:
|
||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(self._config, f, indent=4, ensure_ascii=False)
|
||||
logger.info(f"配置已保存: {self.config_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"保存配置失败: {e}")
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""获取配置项"""
|
||||
keys = key.split(".")
|
||||
value = self._config
|
||||
|
||||
for k in keys:
|
||||
if isinstance(value, dict) and k in value:
|
||||
value = value[k]
|
||||
else:
|
||||
return default
|
||||
|
||||
return value
|
||||
|
||||
def set(self, key: str, value: Any):
|
||||
"""设置配置项"""
|
||||
keys = key.split(".")
|
||||
config = self._config
|
||||
|
||||
for k in keys[:-1]:
|
||||
if k not in config:
|
||||
config[k] = {}
|
||||
config = config[k]
|
||||
|
||||
config[keys[-1]] = value
|
||||
|
||||
def get_all(self) -> Dict:
|
||||
"""获取所有配置"""
|
||||
return self._config.copy()
|
||||
82
src/utils/logger.py
Normal file
82
src/utils/logger.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
日志配置 - 使用标准库logging
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def setup_logger(log_dir: Path = None, level: str = "INFO"):
|
||||
"""
|
||||
配置日志系统
|
||||
|
||||
Args:
|
||||
log_dir: 日志文件目录,默认为项目根目录下的logs文件夹
|
||||
level: 日志级别
|
||||
"""
|
||||
if log_dir is None:
|
||||
log_dir = Path(__file__).parent.parent.parent / "logs"
|
||||
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 设置日志级别
|
||||
log_level = getattr(logging, level.upper(), logging.INFO)
|
||||
|
||||
# 配置根日志记录器
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 清除现有处理器
|
||||
logger.handlers.clear()
|
||||
|
||||
# 控制台处理器
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(log_level)
|
||||
console_format = logging.Formatter(
|
||||
'%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d - %(message)s',
|
||||
datefmt='%H:%M:%S'
|
||||
)
|
||||
console_handler.setFormatter(console_format)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# 文件处理器
|
||||
log_file = log_dir / f"luoluo_{datetime.now().strftime('%Y-%m-%d')}.log"
|
||||
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_format = logging.Formatter(
|
||||
'%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
file_handler.setFormatter(file_format)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
# 创建一个简单的logger别名,兼容loguru的接口
|
||||
class SimpleLogger:
|
||||
"""简单的logger包装类,提供类似loguru的接口"""
|
||||
|
||||
def __init__(self):
|
||||
self._logger = logging.getLogger("LuoLuoTool")
|
||||
|
||||
def debug(self, message):
|
||||
self._logger.debug(message)
|
||||
|
||||
def info(self, message):
|
||||
self._logger.info(message)
|
||||
|
||||
def warning(self, message):
|
||||
self._logger.warning(message)
|
||||
|
||||
def error(self, message):
|
||||
self._logger.error(message)
|
||||
|
||||
def critical(self, message):
|
||||
self._logger.critical(message)
|
||||
|
||||
|
||||
# 全局logger实例
|
||||
logger = SimpleLogger()
|
||||
76
test_integration.py
Normal file
76
test_integration.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
集成测试 - 测试真实游戏窗口捕获
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
from src.core.game_window import GameWindowManager
|
||||
|
||||
def test_find_by_exe():
|
||||
"""测试通过exe查找"""
|
||||
print("=== 测试通过exe查找游戏窗口 ===")
|
||||
manager = GameWindowManager()
|
||||
result = manager.find_window_by_exe('ycgame.exe')
|
||||
|
||||
if result:
|
||||
print("成功!")
|
||||
print(f" HWND: {manager.hwnd}")
|
||||
print(f" PID: {manager.process_id}")
|
||||
print(f" 大小: {manager.client_size}")
|
||||
info = manager.get_window_info()
|
||||
print(f" 标题: {info['title']}")
|
||||
print(f" 路径: {info['exe_path']}")
|
||||
return True
|
||||
else:
|
||||
print("未找到游戏窗口")
|
||||
return False
|
||||
|
||||
def test_find_by_title():
|
||||
"""测试通过标题查找"""
|
||||
print("\n=== 测试通过标题查找游戏窗口 ===")
|
||||
manager = GameWindowManager()
|
||||
result = manager.find_window_by_title('桃源深处有人家')
|
||||
|
||||
if result:
|
||||
print("成功!")
|
||||
print(f" HWND: {manager.hwnd}")
|
||||
print(f" PID: {manager.process_id}")
|
||||
print(f" 大小: {manager.client_size}")
|
||||
info = manager.get_window_info()
|
||||
print(f" 标题: {info['title']}")
|
||||
return True
|
||||
else:
|
||||
print("未找到游戏窗口")
|
||||
return False
|
||||
|
||||
def test_window_operations():
|
||||
"""测试窗口操作"""
|
||||
print("\n=== 测试窗口操作 ===")
|
||||
manager = GameWindowManager()
|
||||
|
||||
if not manager.capture_window():
|
||||
print("游戏未运行,跳过测试")
|
||||
return False
|
||||
|
||||
print("窗口已捕获")
|
||||
|
||||
# 测试获取信息
|
||||
info = manager.get_window_info()
|
||||
print(f"窗口信息: {info}")
|
||||
|
||||
# 测试置前
|
||||
print("尝试将窗口置前...")
|
||||
manager.bring_to_front()
|
||||
print("完成")
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("开始集成测试\n")
|
||||
|
||||
test_find_by_exe()
|
||||
test_find_by_title()
|
||||
test_window_operations()
|
||||
|
||||
print("\n测试完成")
|
||||
374
tests/test_game_window.py
Normal file
374
tests/test_game_window.py
Normal file
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
GameWindowManager 测试用例
|
||||
|
||||
测试环境要求:
|
||||
- Windows 操作系统
|
||||
- Python 3.9+
|
||||
- 安装依赖: pywin32, psutil
|
||||
|
||||
运行测试:
|
||||
python -m pytest tests/test_game_window.py -v
|
||||
python -m pytest tests/test_game_window.py::TestGameWindowManager::test_find_window_by_exe -v
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import time
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
import psutil
|
||||
|
||||
# 添加项目根目录到路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.core.game_window import GameWindowManager
|
||||
|
||||
|
||||
class TestGameWindowManager(unittest.TestCase):
|
||||
"""GameWindowManager 测试类"""
|
||||
|
||||
def setUp(self):
|
||||
"""每个测试方法前执行"""
|
||||
self.manager = GameWindowManager()
|
||||
|
||||
def tearDown(self):
|
||||
"""每个测试方法后执行"""
|
||||
self.manager = None
|
||||
|
||||
# ==================== 基础属性测试 ====================
|
||||
|
||||
def test_initial_state(self):
|
||||
"""测试初始状态"""
|
||||
self.assertIsNone(self.manager.hwnd)
|
||||
self.assertIsNone(self.manager.process_id)
|
||||
self.assertFalse(self.manager.is_window_captured)
|
||||
self.assertEqual(self.manager.client_size, (0, 0))
|
||||
self.assertEqual(self.manager.window_rect, (0, 0, 0, 0))
|
||||
|
||||
def test_game_constants(self):
|
||||
"""测试游戏常量配置"""
|
||||
self.assertEqual(self.manager.GAME_EXE_NAME, "ycgame.exe")
|
||||
self.assertEqual(self.manager.GAME_WINDOW_TITLE, "桃源深处有人家")
|
||||
|
||||
# ==================== 模拟窗口测试 ====================
|
||||
|
||||
@patch('src.core.game_window.win32gui')
|
||||
@patch('src.core.game_window.win32process')
|
||||
@patch('src.core.game_window.psutil')
|
||||
def test_find_window_by_exe_success(self, mock_psutil, mock_win32process, mock_win32gui):
|
||||
"""测试通过exe名成功查找窗口"""
|
||||
# 模拟窗口句柄和进程ID
|
||||
mock_hwnd = 12345
|
||||
mock_pid = 67890
|
||||
|
||||
# 设置模拟返回值
|
||||
mock_win32gui.IsWindowVisible.return_value = True
|
||||
mock_win32gui.GetWindowText.return_value = "桃源深处有人家"
|
||||
mock_win32gui.GetWindowRect.return_value = (100, 100, 1100, 700)
|
||||
mock_win32process.GetWindowThreadProcessId.return_value = (None, mock_pid)
|
||||
|
||||
# 模拟进程对象
|
||||
mock_process = Mock()
|
||||
mock_process.exe.return_value = "C:\\Game\\ycgame.exe"
|
||||
mock_psutil.Process.return_value = mock_process
|
||||
|
||||
# 模拟EnumWindows回调
|
||||
def mock_enum_windows(callback, extra):
|
||||
callback(mock_hwnd, extra)
|
||||
return True
|
||||
mock_win32gui.EnumWindows = mock_enum_windows
|
||||
|
||||
# 执行测试
|
||||
result = self.manager.find_window_by_exe("ycgame.exe")
|
||||
|
||||
# 验证结果
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(self.manager.hwnd, mock_hwnd)
|
||||
self.assertEqual(self.manager.process_id, mock_pid)
|
||||
self.assertTrue(self.manager.is_window_captured)
|
||||
self.assertEqual(self.manager.client_size, (1000, 600))
|
||||
|
||||
@patch('src.core.game_window.win32gui')
|
||||
@patch('src.core.game_window.win32process')
|
||||
@patch('src.core.game_window.psutil')
|
||||
def test_find_window_by_exe_not_found(self, mock_psutil, mock_win32process, mock_win32gui):
|
||||
"""测试通过exe名未找到窗口"""
|
||||
# 模拟没有匹配的窗口
|
||||
def mock_enum_windows(callback, extra):
|
||||
return True
|
||||
mock_win32gui.EnumWindows = mock_enum_windows
|
||||
|
||||
result = self.manager.find_window_by_exe("nonexistent.exe")
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertIsNone(self.manager.hwnd)
|
||||
|
||||
@patch('src.core.game_window.win32gui')
|
||||
@patch('src.core.game_window.win32process')
|
||||
def test_find_window_by_title_success(self, mock_win32process, mock_win32gui):
|
||||
"""测试通过标题成功查找窗口"""
|
||||
mock_hwnd = 12345
|
||||
mock_pid = 67890
|
||||
|
||||
mock_win32gui.FindWindow.return_value = mock_hwnd
|
||||
mock_win32gui.GetWindowRect.return_value = (100, 100, 1100, 700)
|
||||
mock_win32process.GetWindowThreadProcessId.return_value = (None, mock_pid)
|
||||
|
||||
result = self.manager.find_window_by_title("桃源深处有人家")
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(self.manager.hwnd, mock_hwnd)
|
||||
self.assertEqual(self.manager.process_id, mock_pid)
|
||||
|
||||
@patch('src.core.game_window.win32gui')
|
||||
def test_find_window_by_title_not_found(self, mock_win32gui):
|
||||
"""测试通过标题未找到窗口"""
|
||||
mock_win32gui.FindWindow.return_value = 0
|
||||
|
||||
# 模拟EnumWindows也没有找到
|
||||
def mock_enum_windows(callback, extra):
|
||||
return True
|
||||
mock_win32gui.EnumWindows = mock_enum_windows
|
||||
|
||||
result = self.manager.find_window_by_title("不存在的窗口")
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertIsNone(self.manager.hwnd)
|
||||
|
||||
# ==================== 综合查找测试 ====================
|
||||
|
||||
@patch.object(GameWindowManager, 'find_window_by_exe')
|
||||
@patch.object(GameWindowManager, 'find_window_by_title')
|
||||
def test_find_window_use_exe_success(self, mock_find_by_title, mock_find_by_exe):
|
||||
"""测试综合查找 - 通过exe成功"""
|
||||
mock_find_by_exe.return_value = True
|
||||
|
||||
result = self.manager.find_window(use_exe=True)
|
||||
|
||||
self.assertTrue(result)
|
||||
mock_find_by_exe.assert_called_once()
|
||||
mock_find_by_title.assert_not_called()
|
||||
|
||||
@patch.object(GameWindowManager, 'find_window_by_exe')
|
||||
@patch.object(GameWindowManager, 'find_window_by_title')
|
||||
def test_find_window_use_exe_fail_then_title(self, mock_find_by_title, mock_find_by_exe):
|
||||
"""测试综合查找 - exe失败后用标题"""
|
||||
mock_find_by_exe.return_value = False
|
||||
mock_find_by_title.return_value = True
|
||||
|
||||
result = self.manager.find_window(use_exe=True)
|
||||
|
||||
self.assertTrue(result)
|
||||
mock_find_by_exe.assert_called_once()
|
||||
mock_find_by_title.assert_called_once()
|
||||
|
||||
@patch.object(GameWindowManager, 'find_window_by_title')
|
||||
def test_find_window_use_title_only(self, mock_find_by_title):
|
||||
"""测试综合查找 - 只用标题"""
|
||||
mock_find_by_title.return_value = True
|
||||
|
||||
result = self.manager.find_window(use_exe=False)
|
||||
|
||||
self.assertTrue(result)
|
||||
mock_find_by_title.assert_called_once()
|
||||
|
||||
# ==================== 窗口操作测试 ====================
|
||||
|
||||
@patch('src.core.game_window.win32gui')
|
||||
def test_bring_to_front(self, mock_win32gui):
|
||||
"""测试窗口置前"""
|
||||
self.manager._hwnd = 12345
|
||||
mock_win32gui.IsWindow.return_value = True
|
||||
|
||||
self.manager.bring_to_front()
|
||||
|
||||
mock_win32gui.SetForegroundWindow.assert_called_once_with(12345)
|
||||
|
||||
@patch('src.core.game_window.win32gui')
|
||||
def test_bring_to_front_not_captured(self, mock_win32gui):
|
||||
"""测试窗口未捕获时不执行置前"""
|
||||
mock_win32gui.IsWindow.return_value = False
|
||||
|
||||
self.manager.bring_to_front()
|
||||
|
||||
mock_win32gui.SetForegroundWindow.assert_not_called()
|
||||
|
||||
@patch('src.core.game_window.win32gui')
|
||||
def test_click(self, mock_win32gui):
|
||||
"""测试点击坐标转换"""
|
||||
self.manager._hwnd = 12345
|
||||
mock_win32gui.IsWindow.return_value = True
|
||||
mock_win32gui.GetWindowRect.return_value = (100, 100, 1100, 700)
|
||||
|
||||
# 执行点击
|
||||
self.manager.click(500, 300)
|
||||
|
||||
# 验证坐标转换 (窗口左上角100,100 + 相对坐标500,300 = 屏幕坐标600,400)
|
||||
# 注意:实际点击功能还未实现,这里只测试坐标转换逻辑
|
||||
|
||||
# ==================== 窗口信息测试 ====================
|
||||
|
||||
@patch('src.core.game_window.win32gui')
|
||||
@patch('src.core.game_window.psutil')
|
||||
def test_get_window_info_captured(self, mock_psutil, mock_win32gui):
|
||||
"""测试获取窗口信息 - 已捕获"""
|
||||
mock_hwnd = 12345
|
||||
mock_pid = 67890
|
||||
|
||||
self.manager._hwnd = mock_hwnd
|
||||
self.manager._process_id = mock_pid
|
||||
mock_win32gui.IsWindow.return_value = True
|
||||
mock_win32gui.GetWindowText.return_value = "桃源深处有人家"
|
||||
mock_win32gui.GetWindowRect.return_value = (100, 100, 1100, 700)
|
||||
|
||||
mock_process = Mock()
|
||||
mock_process.exe.return_value = "C:\\Game\\ycgame.exe"
|
||||
mock_psutil.Process.return_value = mock_process
|
||||
|
||||
info = self.manager.get_window_info()
|
||||
|
||||
self.assertTrue(info["captured"])
|
||||
self.assertEqual(info["hwnd"], mock_hwnd)
|
||||
self.assertEqual(info["title"], "桃源深处有人家")
|
||||
self.assertEqual(info["process_id"], mock_pid)
|
||||
self.assertEqual(info["exe_path"], "C:\\Game\\ycgame.exe")
|
||||
self.assertEqual(info["rect"], (100, 100, 1100, 700))
|
||||
self.assertEqual(info["size"], (1000, 600))
|
||||
|
||||
def test_get_window_info_not_captured(self):
|
||||
"""测试获取窗口信息 - 未捕获"""
|
||||
info = self.manager.get_window_info()
|
||||
|
||||
self.assertFalse(info["captured"])
|
||||
self.assertIsNone(info["hwnd"])
|
||||
self.assertIsNone(info["title"])
|
||||
self.assertIsNone(info["process_id"])
|
||||
self.assertIsNone(info["exe_path"])
|
||||
self.assertIsNone(info["rect"])
|
||||
self.assertIsNone(info["size"])
|
||||
|
||||
# ==================== 错误处理测试 ====================
|
||||
|
||||
@patch('src.core.game_window.win32gui')
|
||||
@patch('src.core.game_window.win32process')
|
||||
@patch('src.core.game_window.psutil')
|
||||
def test_find_window_by_exe_access_denied(self, mock_psutil, mock_win32process, mock_win32gui):
|
||||
"""测试访问被拒绝时的处理"""
|
||||
mock_win32gui.IsWindowVisible.return_value = True
|
||||
mock_win32process.GetWindowThreadProcessId.return_value = (None, 12345)
|
||||
mock_psutil.Process.side_effect = psutil.AccessDenied("Access denied")
|
||||
|
||||
def mock_enum_windows(callback, extra):
|
||||
callback(12345, extra)
|
||||
return True
|
||||
mock_win32gui.EnumWindows = mock_enum_windows
|
||||
|
||||
result = self.manager.find_window_by_exe("ycgame.exe")
|
||||
|
||||
# 应该正常处理异常,返回False
|
||||
self.assertFalse(result)
|
||||
|
||||
@patch('src.core.game_window.win32gui')
|
||||
def test_find_window_by_exe_exception(self, mock_win32gui):
|
||||
"""测试异常情况处理"""
|
||||
mock_win32gui.EnumWindows.side_effect = Exception("Test exception")
|
||||
|
||||
result = self.manager.find_window_by_exe("ycgame.exe")
|
||||
|
||||
self.assertFalse(result)
|
||||
|
||||
# ==================== 集成测试(需要实际游戏运行)====================
|
||||
|
||||
@unittest.skip("需要实际游戏运行")
|
||||
def test_integration_find_real_game_window_by_exe(self):
|
||||
"""集成测试:查找真实游戏窗口(通过exe)"""
|
||||
result = self.manager.find_window_by_exe("ycgame.exe")
|
||||
|
||||
if result:
|
||||
print(f"\n找到游戏窗口:")
|
||||
print(f" HWND: {self.manager.hwnd}")
|
||||
print(f" PID: {self.manager.process_id}")
|
||||
print(f" 大小: {self.manager.client_size}")
|
||||
info = self.manager.get_window_info()
|
||||
print(f" 标题: {info['title']}")
|
||||
print(f" 路径: {info['exe_path']}")
|
||||
else:
|
||||
print("\n未找到游戏窗口,请确保游戏已运行")
|
||||
|
||||
# 不强制断言,因为游戏可能未运行
|
||||
|
||||
@unittest.skip("需要实际游戏运行")
|
||||
def test_integration_find_real_game_window_by_title(self):
|
||||
"""集成测试:查找真实游戏窗口(通过标题)"""
|
||||
result = self.manager.find_window_by_title("桃源深处有人家")
|
||||
|
||||
if result:
|
||||
print(f"\n找到游戏窗口:")
|
||||
print(f" HWND: {self.manager.hwnd}")
|
||||
print(f" 大小: {self.manager.client_size}")
|
||||
else:
|
||||
print("\n未找到游戏窗口,请确保游戏已运行")
|
||||
|
||||
@unittest.skip("需要实际游戏运行")
|
||||
def test_integration_window_operations(self):
|
||||
"""集成测试:窗口操作"""
|
||||
# 先捕获窗口
|
||||
if not self.manager.capture_window():
|
||||
self.skipTest("游戏未运行,跳过测试")
|
||||
|
||||
# 测试获取信息
|
||||
info = self.manager.get_window_info()
|
||||
self.assertTrue(info["captured"])
|
||||
|
||||
# 测试置前(不验证结果,只是确保不报错)
|
||||
try:
|
||||
self.manager.bring_to_front()
|
||||
except Exception as e:
|
||||
self.fail(f"bring_to_front 抛出异常: {e}")
|
||||
|
||||
# 等待一下观察效果
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
class TestGameWindowManagerPerformance(unittest.TestCase):
|
||||
"""性能测试"""
|
||||
|
||||
@patch('src.core.game_window.win32gui')
|
||||
@patch('src.core.game_window.win32process')
|
||||
@patch('src.core.game_window.psutil')
|
||||
def test_find_window_performance(self, mock_psutil, mock_win32process, mock_win32gui):
|
||||
"""测试查找窗口性能"""
|
||||
# 设置模拟数据
|
||||
mock_win32gui.IsWindowVisible.return_value = True
|
||||
mock_win32gui.GetWindowText.return_value = "桃源深处有人家"
|
||||
mock_win32gui.GetWindowRect.return_value = (100, 100, 1100, 700)
|
||||
mock_win32process.GetWindowThreadProcessId.return_value = (None, 12345)
|
||||
|
||||
mock_process = Mock()
|
||||
mock_process.exe.return_value = "C:\\Game\\ycgame.exe"
|
||||
mock_psutil.Process.return_value = mock_process
|
||||
|
||||
def mock_enum_windows(callback, extra):
|
||||
for i in range(100): # 模拟100个窗口
|
||||
callback(10000 + i, extra)
|
||||
return True
|
||||
mock_win32gui.EnumWindows = mock_enum_windows
|
||||
|
||||
manager = GameWindowManager()
|
||||
|
||||
# 测量查找时间
|
||||
import time
|
||||
start = time.time()
|
||||
result = manager.find_window_by_exe("ycgame.exe")
|
||||
elapsed = time.time() - start
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertLess(elapsed, 1.0, "查找窗口应该在一秒内完成")
|
||||
print(f"\n查找100个窗口耗时: {elapsed:.3f}秒")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 运行测试
|
||||
unittest.main(verbosity=2)
|
||||
356
tests/test_input_simulator.py
Normal file
356
tests/test_input_simulator.py
Normal file
@@ -0,0 +1,356 @@
|
||||
"""
|
||||
InputSimulator 测试用例
|
||||
|
||||
测试环境要求:
|
||||
- Windows 操作系统
|
||||
- Python 3.9+
|
||||
- 安装依赖: pywin32
|
||||
|
||||
运行测试:
|
||||
python -m pytest tests/test_input_simulator.py -v
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import time
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock, call
|
||||
|
||||
# 添加项目根目录到路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.core.input_simulator import InputSimulator, Point, SwipePath
|
||||
from src.core.game_window import GameWindowManager
|
||||
|
||||
|
||||
class TestInputSimulator(unittest.TestCase):
|
||||
"""InputSimulator 测试类"""
|
||||
|
||||
def setUp(self):
|
||||
"""每个测试方法前执行"""
|
||||
self.mock_window = Mock(spec=GameWindowManager)
|
||||
self.mock_window.is_window_captured = True
|
||||
self.mock_window.window_rect = (100, 100, 1100, 700) # left, top, right, bottom
|
||||
|
||||
self.simulator = InputSimulator(self.mock_window)
|
||||
|
||||
def tearDown(self):
|
||||
"""每个测试方法后执行"""
|
||||
self.simulator = None
|
||||
|
||||
# ==================== 基础功能测试 ====================
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_click_short(self, mock_win32api):
|
||||
"""测试短按点击"""
|
||||
self.simulator.click(100, 200)
|
||||
|
||||
# 验证鼠标移动
|
||||
mock_win32api.SetCursorPos.assert_called()
|
||||
|
||||
# 验证按下和释放
|
||||
mock_win32api.mouse_event.assert_any_call(
|
||||
unittest.mock.ANY, 0, 0, 0, 0 # MOUSEEVENTF_LEFTDOWN
|
||||
)
|
||||
mock_win32api.mouse_event.assert_any_call(
|
||||
unittest.mock.ANY, 0, 0, 0, 0 # MOUSEEVENTF_LEFTUP
|
||||
)
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_click_long(self, mock_win32api):
|
||||
"""测试长按"""
|
||||
start_time = time.time()
|
||||
self.simulator.click(100, 200, duration=0.5)
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# 验证长按时间(允许误差)
|
||||
self.assertGreaterEqual(elapsed, 0.5)
|
||||
self.assertLess(elapsed, 0.7)
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_click_different_buttons(self, mock_win32api):
|
||||
"""测试不同鼠标按钮"""
|
||||
# 左键
|
||||
self.simulator.click(100, 200, button="left")
|
||||
# 右键
|
||||
self.simulator.click(100, 200, button="right")
|
||||
# 中键
|
||||
self.simulator.click(100, 200, button="middle")
|
||||
|
||||
# 验证调用了3次按下和3次释放
|
||||
self.assertEqual(mock_win32api.mouse_event.call_count, 6)
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_click_window_not_captured(self, mock_win32api):
|
||||
"""测试窗口未捕获时抛出异常"""
|
||||
self.mock_window.is_window_captured = False
|
||||
|
||||
with self.assertRaises(RuntimeError) as context:
|
||||
self.simulator.click(100, 200)
|
||||
|
||||
self.assertIn("窗口未捕获", str(context.exception))
|
||||
|
||||
# ==================== 滑动测试 ====================
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_swipe_basic(self, mock_win32api):
|
||||
"""测试基础滑动"""
|
||||
self.simulator.swipe(100, 200, 300, 400, duration=0.1)
|
||||
|
||||
# 验证鼠标移动被多次调用(滑动过程)
|
||||
self.assertGreater(mock_win32api.SetCursorPos.call_count, 5)
|
||||
|
||||
# 验证按下和释放
|
||||
mock_win32api.mouse_event.assert_any_call(
|
||||
unittest.mock.ANY, 0, 0, 0, 0 # MOUSEEVENTF_LEFTDOWN
|
||||
)
|
||||
mock_win32api.mouse_event.assert_any_call(
|
||||
unittest.mock.ANY, 0, 0, 0, 0 # MOUSEEVENTF_LEFTUP
|
||||
)
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_swipe_duration(self, mock_win32api):
|
||||
"""测试滑动持续时间"""
|
||||
start_time = time.time()
|
||||
self.simulator.swipe(100, 200, 300, 400, duration=0.3)
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# 验证滑动时间(允许误差)
|
||||
self.assertGreaterEqual(elapsed, 0.3)
|
||||
self.assertLess(elapsed, 0.5)
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_swipe_coordinate_conversion(self, mock_win32api):
|
||||
"""测试滑动坐标转换"""
|
||||
self.simulator.swipe(0, 0, 100, 100)
|
||||
|
||||
# 获取所有SetCursorPos调用
|
||||
calls = mock_win32api.SetCursorPos.call_args_list
|
||||
|
||||
# 第一个调用应该是起点(窗口左上角100,100 + 相对坐标0,0 = 屏幕坐标100,100)
|
||||
first_call = calls[0]
|
||||
self.assertEqual(first_call[0][0], (100, 100))
|
||||
|
||||
# 最后一个调用应该是终点(窗口左上角100,100 + 相对坐标100,100 = 屏幕坐标200,200)
|
||||
last_call = calls[-1]
|
||||
self.assertEqual(last_call[0][0], (200, 200))
|
||||
|
||||
# ==================== 键盘测试 ====================
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_key_press_letter(self, mock_win32api):
|
||||
"""测试字母按键"""
|
||||
self.simulator.key_press('a')
|
||||
|
||||
# 验证按下和释放
|
||||
self.assertEqual(mock_win32api.keybd_event.call_count, 2)
|
||||
|
||||
# 验证按下(第二个参数为0表示按下)
|
||||
mock_win32api.keybd_event.assert_any_call(ord('A'), 0, 0, 0)
|
||||
|
||||
# 验证释放(第三个参数包含KEYEVENTF_KEYUP)
|
||||
mock_win32api.keybd_event.assert_any_call(
|
||||
ord('A'), 0, unittest.mock.ANY, 0
|
||||
)
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_key_press_special(self, mock_win32api):
|
||||
"""测试特殊按键"""
|
||||
special_keys = ['enter', 'esc', 'space', 'tab', 'f1', 'ctrl']
|
||||
|
||||
for key in special_keys:
|
||||
self.simulator.key_press(key)
|
||||
|
||||
# 每个按键应该有按下和释放两个调用
|
||||
self.assertEqual(mock_win32api.keybd_event.call_count, len(special_keys) * 2)
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_key_press_unknown(self, mock_win32api):
|
||||
"""测试未知按键"""
|
||||
self.simulator.key_press('unknown_key')
|
||||
|
||||
# 不应该调用keybd_event
|
||||
mock_win32api.keybd_event.assert_not_called()
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_key_down_up(self, mock_win32api):
|
||||
"""测试按住和释放按键"""
|
||||
self.simulator.key_down('shift')
|
||||
self.simulator.key_up('shift')
|
||||
|
||||
# 验证只调用了两次(一次按下,一次释放)
|
||||
self.assertEqual(mock_win32api.keybd_event.call_count, 2)
|
||||
|
||||
# 第一次是按下(第3个参数为0)
|
||||
first_call = mock_win32api.keybd_event.call_args_list[0]
|
||||
self.assertEqual(first_call[0][2], 0) # dwFlags是第3个位置参数
|
||||
|
||||
# 第二次是释放(第3个参数不为0)
|
||||
second_call = mock_win32api.keybd_event.call_args_list[1]
|
||||
self.assertNotEqual(second_call[0][2], 0)
|
||||
|
||||
# ==================== 便捷方法测试 ====================
|
||||
|
||||
@patch.object(InputSimulator, 'click')
|
||||
def test_double_click(self, mock_click):
|
||||
"""测试双击"""
|
||||
self.simulator.double_click(100, 200)
|
||||
|
||||
# 验证click被调用了两次
|
||||
self.assertEqual(mock_click.call_count, 2)
|
||||
mock_click.assert_any_call(100, 200, duration=0, button='left')
|
||||
|
||||
@patch.object(InputSimulator, 'click')
|
||||
def test_long_press(self, mock_click):
|
||||
"""测试长按便捷方法"""
|
||||
self.simulator.long_press(100, 200, duration=2.0)
|
||||
|
||||
# 验证调用了click并传递了duration
|
||||
mock_click.assert_called_once_with(100, 200, duration=2.0, button='left')
|
||||
|
||||
@patch.object(InputSimulator, 'swipe')
|
||||
def test_drag(self, mock_swipe):
|
||||
"""测试拖拽便捷方法"""
|
||||
self.simulator.drag(100, 200, 300, 400, duration=1.0, button='left')
|
||||
|
||||
# 验证调用了swipe
|
||||
mock_swipe.assert_called_once_with(100, 200, 300, 400, 1.0, 'left')
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_scroll(self, mock_win32api):
|
||||
"""测试滚动"""
|
||||
self.simulator.scroll(500, 300, delta=-3)
|
||||
|
||||
# 验证鼠标移动
|
||||
mock_win32api.SetCursorPos.assert_called_once()
|
||||
|
||||
# 验证滚动事件(delta * 120 = -360)
|
||||
mock_win32api.mouse_event.assert_called_once_with(
|
||||
unittest.mock.ANY, 0, 0, -360, 0
|
||||
)
|
||||
|
||||
# ==================== 坐标转换测试 ====================
|
||||
|
||||
def test_coordinate_conversion(self):
|
||||
"""测试坐标转换"""
|
||||
# 窗口位置 (100, 100),点击窗口内 (50, 50)
|
||||
screen_coords = self.simulator._to_screen_coords(50, 50)
|
||||
|
||||
# 屏幕坐标应该是 (150, 150)
|
||||
self.assertEqual(screen_coords, (150, 150))
|
||||
|
||||
def test_coordinate_conversion_different_window(self):
|
||||
"""测试不同窗口位置的坐标转换"""
|
||||
self.mock_window.window_rect = (200, 100, 1200, 700)
|
||||
|
||||
screen_coords = self.simulator._to_screen_coords(100, 200)
|
||||
|
||||
# 屏幕坐标应该是 (300, 300)
|
||||
self.assertEqual(screen_coords, (300, 300))
|
||||
|
||||
# ==================== 性能测试 ====================
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_click_performance(self, mock_win32api):
|
||||
"""测试点击性能"""
|
||||
start = time.time()
|
||||
|
||||
# 执行10次点击(减少次数避免超时)
|
||||
for i in range(10):
|
||||
self.simulator.click(i, i)
|
||||
|
||||
elapsed = time.time() - start
|
||||
|
||||
# 10次点击应该在3秒内完成
|
||||
self.assertLess(elapsed, 3.0)
|
||||
print(f"\n10次点击耗时: {elapsed:.3f}秒")
|
||||
|
||||
@patch('src.core.input_simulator.win32api')
|
||||
def test_swipe_smoothness(self, mock_win32api):
|
||||
"""测试滑动平滑度(步数)"""
|
||||
self.simulator.swipe(0, 0, 1000, 1000, duration=1.0)
|
||||
|
||||
# 获取SetCursorPos调用次数(应该至少有60次,每秒60步)
|
||||
call_count = mock_win32api.SetCursorPos.call_count
|
||||
|
||||
# 验证有足够的步数保证平滑
|
||||
self.assertGreaterEqual(call_count, 60)
|
||||
print(f"\n滑动步数: {call_count}")
|
||||
|
||||
|
||||
class TestPoint(unittest.TestCase):
|
||||
"""Point 数据类测试"""
|
||||
|
||||
def test_point_creation(self):
|
||||
"""测试创建Point"""
|
||||
p = Point(10, 20)
|
||||
self.assertEqual(p.x, 10)
|
||||
self.assertEqual(p.y, 20)
|
||||
|
||||
def test_point_addition(self):
|
||||
"""测试Point相加"""
|
||||
p1 = Point(10, 20)
|
||||
p2 = Point(5, 5)
|
||||
result = p1 + p2
|
||||
|
||||
self.assertEqual(result.x, 15)
|
||||
self.assertEqual(result.y, 25)
|
||||
|
||||
def test_point_iteration(self):
|
||||
"""测试Point迭代"""
|
||||
p = Point(10, 20)
|
||||
coords = list(p)
|
||||
|
||||
self.assertEqual(coords, [10, 20])
|
||||
|
||||
|
||||
class TestIntegration(unittest.TestCase):
|
||||
"""集成测试(需要实际游戏运行)"""
|
||||
|
||||
@unittest.skip("需要实际游戏运行")
|
||||
def test_real_click(self):
|
||||
"""测试真实点击"""
|
||||
from src.core.game_window import GameWindowManager
|
||||
|
||||
window = GameWindowManager()
|
||||
if not window.capture_window():
|
||||
self.skipTest("游戏未运行")
|
||||
|
||||
simulator = InputSimulator(window)
|
||||
|
||||
# 在游戏窗口中心点击
|
||||
size = window.client_size
|
||||
center_x = size[0] // 2
|
||||
center_y = size[1] // 2
|
||||
|
||||
print(f"\n将在游戏窗口中心 ({center_x}, {center_y}) 点击")
|
||||
simulator.click(center_x, center_y)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
@unittest.skip("需要实际游戏运行")
|
||||
def test_real_swipe(self):
|
||||
"""测试真实滑动"""
|
||||
from src.core.game_window import GameWindowManager
|
||||
|
||||
window = GameWindowManager()
|
||||
if not window.capture_window():
|
||||
self.skipTest("游戏未运行")
|
||||
|
||||
simulator = InputSimulator(window)
|
||||
|
||||
size = window.client_size
|
||||
start_x = size[0] // 4
|
||||
start_y = size[1] // 2
|
||||
end_x = size[0] * 3 // 4
|
||||
end_y = size[1] // 2
|
||||
|
||||
print(f"\n将从 ({start_x}, {start_y}) 滑动到 ({end_x}, {end_y})")
|
||||
simulator.swipe(start_x, start_y, end_x, end_y, duration=1.0)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user