feat: add offer reward claim workflow
This commit is contained in:
1 parent
870ca82e8a
commit
b6bd78dc66
25 files changed
+1028
-5
No files matched your search
@@ -0,0 +1,170 @@
|
||||
import json
|
||||
import queue
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
from .engine import Desktop, Matcher, Region, Runner, load_flow
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
LOCAL = ROOT / 'local'
|
||||
|
||||
|
||||
class Selector(tk.Toplevel):
|
||||
def __init__(self, parent, screenshot, on_select):
|
||||
super().__init__(parent)
|
||||
self.title('拖动框选游戏内容:不包含微信标题栏和窗口边框')
|
||||
self.original = screenshot
|
||||
self.on_select = on_select
|
||||
ratio = min(1000 / screenshot.width, 700 / screenshot.height, 1)
|
||||
self.preview_size = (round(screenshot.width * ratio), round(screenshot.height * ratio))
|
||||
self.photo = ImageTk.PhotoImage(screenshot.resize(self.preview_size))
|
||||
self.canvas = tk.Canvas(self, width=self.preview_size[0], height=self.preview_size[1], highlightthickness=0)
|
||||
self.canvas.pack()
|
||||
self.canvas.create_image(0, 0, image=self.photo, anchor='nw')
|
||||
self.start = None
|
||||
self.rect = None
|
||||
self.canvas.bind('<ButtonPress-1>', self.press)
|
||||
self.canvas.bind('<B1-Motion>', self.drag)
|
||||
self.canvas.bind('<ButtonRelease-1>', self.release)
|
||||
|
||||
def press(self, event):
|
||||
self.start = (event.x, event.y)
|
||||
if self.rect:
|
||||
self.canvas.delete(self.rect)
|
||||
self.rect = self.canvas.create_rectangle(event.x, event.y, event.x, event.y, outline='#ff3333', width=3)
|
||||
|
||||
def drag(self, event):
|
||||
if self.start:
|
||||
self.canvas.coords(self.rect, *self.start, event.x, event.y)
|
||||
|
||||
def release(self, event):
|
||||
if not self.start:
|
||||
return
|
||||
x1, x2 = sorted((self.start[0], event.x))
|
||||
y1, y2 = sorted((self.start[1], event.y))
|
||||
sx = self.original.width / self.preview_size[0]
|
||||
sy = self.original.height / self.preview_size[1]
|
||||
region = Region(round(x1 * sx), round(y1 * sy), round((x2 - x1) * sx), round((y2 - y1) * sy))
|
||||
try:
|
||||
region.validate(self.original.size)
|
||||
except Exception as exc:
|
||||
messagebox.showerror('重新框选', str(exc), parent=self)
|
||||
return
|
||||
self.on_select(region)
|
||||
self.destroy()
|
||||
|
||||
|
||||
class App(tk.Tk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title('蔚蓝星球 · 本地自动化')
|
||||
self.geometry('720x520')
|
||||
self.region = None
|
||||
self.events = queue.Queue()
|
||||
self.stop = threading.Event()
|
||||
self.busy = False
|
||||
self.flow = load_flow(ROOT / 'workflows' / 'pig_claim.json')
|
||||
self.matcher = Matcher(ROOT / 'assets')
|
||||
self.status = tk.StringVar(value='请先将游戏放在主屏幕,打开首页并校准游戏内容区域。')
|
||||
ttk.Label(self, text='第一条流程:小金猪挂机奖励领取', font=('', 18)).pack(pady=16)
|
||||
ttk.Label(self, text='开始前请保持游戏窗口可见且位置不变。鼠标移到主屏幕左上角可紧急停止。').pack()
|
||||
bar = ttk.Frame(self)
|
||||
bar.pack(pady=14)
|
||||
self.buttons = []
|
||||
for name, cmd in [('校准游戏区域', self.capture), ('导入截图校准', self.import_image), ('预览流程', lambda: self.launch(True)), ('执行领取', lambda: self.launch(False))]:
|
||||
button = ttk.Button(bar, text=name, command=cmd)
|
||||
button.pack(side='left', padx=4)
|
||||
self.buttons.append(button)
|
||||
ttk.Button(self, text='停止', command=self.stop.set).pack()
|
||||
ttk.Label(self, textvariable=self.status, wraplength=680).pack(pady=10)
|
||||
self.output = tk.Text(self, height=16, wrap='word', state='disabled')
|
||||
self.output.pack(fill='both', expand=True, padx=16, pady=12)
|
||||
self.protocol('WM_DELETE_WINDOW', self.close)
|
||||
config = LOCAL / 'region.json'
|
||||
if config.exists():
|
||||
try:
|
||||
self.region = Region(**json.loads(config.read_text()))
|
||||
self.status.set('已加载游戏区域;移动游戏窗口或更换屏幕后请重新校准。')
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
self.after(100, self.poll)
|
||||
|
||||
def log(self, text):
|
||||
self.events.put(('log', text))
|
||||
|
||||
def set_region(self, region):
|
||||
LOCAL.mkdir(exist_ok=True)
|
||||
(LOCAL / 'region.json').write_text(json.dumps(asdict(region)), encoding='utf-8')
|
||||
self.region = region
|
||||
self.status.set('校准完成。先预览,确认首页识别通过后再执行。')
|
||||
|
||||
def capture(self):
|
||||
self.iconify()
|
||||
self.after(1500, self.take_capture)
|
||||
|
||||
def take_capture(self):
|
||||
try:
|
||||
screenshot = Desktop().screenshot()
|
||||
except Exception as exc:
|
||||
self.deiconify()
|
||||
messagebox.showerror('截图失败', '请检查系统屏幕录制权限。\n' + str(exc))
|
||||
return
|
||||
self.deiconify()
|
||||
Selector(self, screenshot, self.set_region)
|
||||
|
||||
def import_image(self):
|
||||
name = filedialog.askopenfilename(filetypes=[('主屏幕完整截图', '*.png *.jpg *.jpeg')])
|
||||
if name:
|
||||
with Image.open(name) as image:
|
||||
Selector(self, image.convert('RGB').copy(), self.set_region)
|
||||
|
||||
def launch(self, dry_run):
|
||||
if self.busy:
|
||||
return
|
||||
if self.region is None:
|
||||
messagebox.showinfo('需要校准', '请先校准游戏内容区域。')
|
||||
return
|
||||
self.busy = True
|
||||
self.stop.clear()
|
||||
for button in self.buttons:
|
||||
button.configure(state='disabled')
|
||||
self.status.set('正在预览…' if dry_run else '3 秒后执行,请将游戏放到前台。')
|
||||
region = self.region
|
||||
def work():
|
||||
try:
|
||||
if self.stop.wait(3):
|
||||
return
|
||||
Runner(Desktop(), self.matcher, region, self.log, self.stop).run(self.flow, dry_run)
|
||||
except Exception as exc:
|
||||
self.log('流程停止:' + str(exc))
|
||||
finally:
|
||||
self.events.put(('done', None))
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def poll(self):
|
||||
while not self.events.empty():
|
||||
kind, value = self.events.get_nowait()
|
||||
if kind == 'log':
|
||||
self.output.configure(state='normal')
|
||||
self.output.insert('end', value + '\n')
|
||||
self.output.see('end')
|
||||
self.output.configure(state='disabled')
|
||||
elif kind == 'done':
|
||||
self.busy = False
|
||||
for button in self.buttons:
|
||||
button.configure(state='normal')
|
||||
self.status.set('本轮已结束,请查看日志中的实际结果。')
|
||||
self.after(100, self.poll)
|
||||
|
||||
def close(self):
|
||||
self.stop.set()
|
||||
self.destroy()
|
||||
|
||||
|
||||
def main():
|
||||
App().mainloop()
|
||||
Reference in new issue
Block a user