Files

112 lines
4.8 KiB
Python

"""Verified offer-claim workflow, with an explicit already-claimed branch."""
import time
from .engine import Desktop, Region, WorkflowError
from .permissions import mouse_permission_granted
class MacGameDesktop(Desktop):
def window_bounds(self):
import Quartz
windows = Quartz.CGWindowListCopyWindowInfo(
Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID)
matches = [w for w in windows
if w.get('kCGWindowName') == '永远的蔚蓝星球'
and w.get('kCGWindowOwnerName') == '微信']
if len(matches) != 1:
raise WorkflowError('请在微信中正常打开一个游戏窗口,不要直接启动 WeApp。')
return dict(matches[0]['kCGWindowBounds'])
def region_for(self, screenshot):
b = self.window_bounds()
width, height = self.api.size()
sx, sy = screenshot.width / width, screenshot.height / height
# This observed WeChat header is 44 logical points high. Expanded
# sidebars change aspect ratio and are rejected by Region.validate.
region = Region(round(b['X'] * sx), round((b['Y'] + 44) * sy),
round(b['Width'] * sx), round((b['Height'] - 44) * sy))
region.validate(screenshot.size)
return region
def activate(self):
if not mouse_permission_granted():
raise WorkflowError('请先为运行宿主授予 macOS 辅助功能权限。')
b = self.window_bounds()
self.region_for(self.screenshot())
# Focus with a click in the observed empty part of the header, so
# the first game-button click is not consumed by window activation.
self.api.moveTo(b['X'] + 80, b['Y'] + 22, duration=0.2)
self.api.mouseDown()
try:
time.sleep(0.2)
finally:
saved = self.api.FAILSAFE
self.api.FAILSAFE = False
try:
self.api.mouseUp()
finally:
self.api.FAILSAFE = saved
time.sleep(0.3)
class OfferRunner:
def __init__(self, desktop, matcher, flow, region_provider, log=print, timeout=12):
self.desktop = desktop
self.matcher = matcher
self.flow = flow
self.region_provider = region_provider
self.log = log
self.timeout = timeout
def wait(self, *names):
deadline = time.monotonic() + self.timeout
previous, count = None, 0
while time.monotonic() < deadline:
screenshot = self.desktop.screenshot()
region = self.region_provider(screenshot)
frame = self.matcher.frame(screenshot, region)
detected = next((name for name in names
if self.matcher.matches(frame, self.flow['states'][name])), None)
count = count + 1 if detected and detected == previous else 1 if detected else 0
previous = detected
if count >= 2:
return detected, screenshot, region
time.sleep(0.25)
raise WorkflowError('未识别到预期页面 ' + '/'.join(names) + ',已停止,不重复领取。')
def click(self, action, observation):
_, screenshot, region = observation
# Stop on movement after the recognition frame instead of clicking
# stale coordinates. A future run can re-locate the moved window.
if self.region_provider(screenshot) != region:
raise WorkflowError('识别后游戏窗口移动,已停止,请重新运行。')
self.desktop.click(self.flow['clicks'][action], region, screenshot.size)
self.log('已执行:' + action)
def run(self, dry_run=False):
home = self.wait('home')
if dry_run:
self.log('首页识别通过。计划:特惠 → 一键领取 → 关闭奖励 → 验证按钮变灰 → 返回首页。未点击。')
return 'preview'
self.click('open', home)
offer = self.wait('available', 'claimed', 'menu')
if offer[0] == 'menu':
self.click('battlepass', offer)
offer = self.wait('available', 'claimed')
if offer[0] == 'claimed':
self.log('一键领取已变灰,当前没有可领取奖励;跳过领取。')
self.click('back', offer)
self.wait('home')
self.log('已确认返回首页。')
return 'already_claimed'
self.click('claim', offer)
reward = self.wait('reward')
self.log('已确认“恭喜获得”奖励弹框。')
self.click('dismiss', reward)
completed = self.wait('claimed')
self.log('已确认一键领取按钮变灰。')
self.click('back', completed)
self.wait('home')
self.log('领取完成,已确认返回首页。')
return 'claimed'