Compare commits

...

4 commits

Author SHA1 Message Date
4c3eaef108 Merge branch 'clue'
All checks were successful
ci/woodpecker/push/check_format Pipeline was successful
2025-06-29 20:29:12 +08:00
6d3640aea0 线索改进 2025-06-29 20:28:54 +08:00
fe6a59b0f6 线索数量获取场景修复
All checks were successful
ci/woodpecker/push/check_format Pipeline was successful
2025-06-29 15:20:15 +08:00
706fa6a4c0 线索相关Solver使用TransitionOn新写法
All checks were successful
ci/woodpecker/push/check_format Pipeline was successful
2025-06-29 15:06:32 +08:00
13 changed files with 356 additions and 286 deletions

BIN
mower/resources/clue/filter_all_on.png (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -1,36 +1,36 @@
from mower.solvers.infra.base_mixin import BaseMixin
from mower.solvers.infra.enter_room import EnterRoomSolver
from datetime import timedelta
from mower.utils.log import logger
from mower.utils.recognize import Scene
from mower.utils.solver import BaseSolver
from mower.utils.solver import BaseSolver, TransitionOn
from .utils import clue_cls
from .utils import ClueMainSolver, clue_cls
class DailySolver(BaseSolver, BaseMixin):
class DailySolver(BaseSolver):
solver_name = "每日线索领取"
solver_max_duration = timedelta(minutes=2)
solver_default_scene = None
def run(self) -> None:
self.success = False
super().run()
def transition(self) -> bool:
if (
scene := self.scene()
) == Scene.INFRA_DETAILS and self.detect_room() == "meeting":
self.tap((500, 1000))
elif scene == Scene.INFRA_CONFIDENTIAL:
if self.animation():
return
@TransitionOn(Scene.INFRA_CONFIDENTIAL)
def _(self):
if self.success:
return True
if self.animation():
return
# 检查是否领过线索
daily_scope = ((1822, 208), (1886, 243))
if self.find("clue/badge_new", scope=daily_scope):
self.tap((1800, 270))
else:
return True
elif scene == Scene.CLUE_DAILY:
@TransitionOn(Scene.CLUE_DAILY)
def _(self):
exit_pos = (1484, 152)
if self.find("clue/icon_notification"):
logger.info("当前获得线索数已经达到最大值")
@ -43,9 +43,11 @@ class DailySolver(BaseSolver, BaseMixin):
# 今日线索已领取,点X退出
self.tap(exit_pos)
self.success = True
elif scene == Scene.CLUE_MESSAGE_BOARD:
self.cback(3, id="infra_back")
elif scene in self.waiting_scene:
@TransitionOn(BaseSolver.waiting_scene)
def _(self):
self.waiting_solver()
else:
EnterRoomSolver().run("meeting", detail=False)
@TransitionOn()
def _(self):
ClueMainSolver().run()

View file

@ -1,13 +1,18 @@
from datetime import timedelta
from mower.solvers.infra.base_mixin import BaseMixin
from mower.solvers.infra.enter_room import EnterRoomSolver
from mower.utils import config
from mower.utils.log import logger
from mower.utils.recognize import Scene
from mower.utils.solver import BaseSolver
from mower.utils.solver import BaseSolver, TransitionOn
from .utils import MeetingSolver
class GetClueCountSolver(BaseSolver, BaseMixin):
solver_name = "线索数量"
solver_max_duration = timedelta(minutes=2)
solver_default_scene = None
def run(self) -> int:
self.res = -1
@ -16,16 +21,19 @@ class GetClueCountSolver(BaseSolver, BaseMixin):
return self.res
raise ValueError("未找到线索数量")
def transition(self) -> bool:
if self.find("meeting_arrange_check_in"):
@TransitionOn(Scene.INFRA_DETAILS)
def _(self):
MeetingSolver().run()
self.res = self.read_screen(
config.recog.img, limit=10, cord=((476, 985), (519, 1020))
)
logger.info(f"当前拥有线索数量为{self.res}")
return True
elif (scene := self.scene()) == Scene.INFRA_CONFIDENTIAL:
self.cback(3, id="infra_back")
elif scene in self.waiting_scene:
@TransitionOn(BaseSolver.waiting_scene)
def _(self):
self.waiting_solver()
else:
EnterRoomSolver().run("meeting", detail=False)
@TransitionOn()
def _(self):
MeetingSolver().run()

View file

@ -1,18 +1,20 @@
from mower.solvers.infra.base_mixin import BaseMixin
from mower.solvers.infra.enter_room import EnterRoomSolver
from datetime import timedelta
from mower.utils import config
from mower.utils.image import cmatch, crop2content, cropimg, loadres, thres2
from mower.utils.log import logger
from mower.utils.rapidocr import ocr_rec
from mower.utils.recognize import Scene
from mower.utils.solver import BaseSolver
from mower.utils.solver import BaseSolver, TransitionOn
from mower.utils.vector import va
from .utils import clue_cls, clue_scope
from .utils import ClueMainSolver, clue_cls, clue_scope
class GiveAwaySolver(BaseSolver, BaseMixin):
class GiveAwaySolver(BaseSolver):
solver_name = "传递线索"
solver_max_duration = timedelta(minutes=2)
solver_default_scene = None
def run(self, clue_count) -> None:
self.clue_count = clue_count
@ -20,28 +22,22 @@ class GiveAwaySolver(BaseSolver, BaseMixin):
return
super().run()
def transition(self) -> bool:
if (
scene := self.scene()
) == Scene.INFRA_DETAILS and self.detect_room() == "meeting":
self.tap((500, 1000))
elif scene == Scene.INFRA_CONFIDENTIAL:
@TransitionOn(Scene.INFRA_CONFIDENTIAL)
def _(self):
self.ctap((1799, 578))
elif scene == Scene.CLUE_GIVE_AWAY:
@TransitionOn(Scene.CLUE_GIVE_AWAY)
def _(self):
if not (config.conf.leifeng_mode or self.clue_count > 9):
self.tap((1868, 54))
return True
if self.animation():
return
if (c := clue_cls("give_away")) is None:
self.tap((1868, 54))
return True
if config.recog.gray[230][600] < 90:
self.ctap(clue_scope["give_away"], 5)
return
if self.find("clue/icon_notification"):
return
if self.detect_product_complete():
if self.find("product_complete"):
return
orange = False
for i in range(4):
@ -63,7 +59,11 @@ class GiveAwaySolver(BaseSolver, BaseMixin):
name = ocr_rec(name)
logger.info(f"{name}送一张线索{c}")
self.tap((1808, 200 + i * 255))
elif scene in self.waiting_scene:
@TransitionOn(BaseSolver.waiting_scene)
def _(self):
self.waiting_solver()
else:
EnterRoomSolver().run("meeting", detail=False)
@TransitionOn()
def _(self):
ClueMainSolver().run()

View file

@ -1,20 +1,20 @@
from datetime import timedelta
from mower.solvers.infra.base_mixin import BaseMixin
from mower.solvers.infra.enter_room import EnterRoomSolver
from mower.utils import config
from mower.utils.scene import Scene
from mower.utils.solver import BaseSolver
from mower.utils.solver import BaseSolver, TransitionOn
from .utils import MeetingSolver
class MessageBoard(BaseSolver, BaseMixin):
class MessageBoard(BaseSolver):
solver_name = "留言板"
solver_max_duration = timedelta(minutes=2)
solver_default_scene = None
def transition(self):
if (
scene := self.scene()
) == Scene.INFRA_DETAILS and self.detect_room() == "meeting":
@TransitionOn(Scene.INFRA_DETAILS)
def _(self):
MeetingSolver().run()
if pos := self.find("clue/title_party"):
self.tap(pos)
return
@ -23,20 +23,20 @@ class MessageBoard(BaseSolver, BaseMixin):
self.ctap(scope)
return
self.tap("clue/interact")
elif scene == Scene.CLUE_MESSAGE_BOARD:
@TransitionOn(Scene.CLUE_MESSAGE_BOARD)
def _(self):
if self.animation():
return
if pos := self.find("clue/message_board_collect"):
self.tap(pos)
return
return True
elif scene in [
Scene.INFRA_CONFIDENTIAL,
Scene.CLUE_MESSAGE_BOARD_FRIEND,
Scene.CLUE_ACCESS_RECORD,
]:
self.cback(3, id="infra_back")
elif scene in self.waiting_scene:
@TransitionOn(BaseSolver.waiting_scene)
def _(self):
self.waiting_solver()
else:
EnterRoomSolver().run("meeting", detail=False)
@TransitionOn()
def _(self):
MeetingSolver().run()

View file

@ -1,12 +1,13 @@
from datetime import timedelta
from mower.solvers.infra.base_mixin import BaseMixin
from mower.solvers.infra.enter_room import EnterRoomSolver
from mower.utils import config
from mower.utils.log import logger
from mower.utils.recognize import Scene
from mower.utils.solver import BaseSolver, TransitionOn
from .utils import MeetingSolver
class PartyTimeSolver(BaseSolver, BaseMixin):
solver_name = "线索交流结束时间"
@ -15,9 +16,7 @@ class PartyTimeSolver(BaseSolver, BaseMixin):
@TransitionOn(Scene.INFRA_DETAILS)
def _(self):
if self.detect_room() != "meeting":
EnterRoomSolver().run("meeting", detail=False)
return
MeetingSolver().run()
if not self.find("clue/party_on"):
config.party_time = None
logger.info("线索交流未开启")
@ -28,7 +27,6 @@ class PartyTimeSolver(BaseSolver, BaseMixin):
return True
if pos := self.find("clue/show_party_details"):
self.tap(pos)
return
@TransitionOn(BaseSolver.waiting_scene)
def _(self):
@ -36,8 +34,4 @@ class PartyTimeSolver(BaseSolver, BaseMixin):
@TransitionOn()
def _(self):
EnterRoomSolver().run("meeting", detail=False)
@TransitionOn(Scene.INFRA_CONFIDENTIAL)
def _(self):
self.cback(3, id="infra_back")
MeetingSolver().run()

View file

@ -1,19 +1,19 @@
from datetime import timedelta
import cv2
from mower.solvers.infra.base_mixin import BaseMixin
from mower.solvers.infra.enter_room import EnterRoomSolver
from mower.utils import config
from mower.utils.image import crop2content, cropimg, loadres, thres2
from mower.utils.log import logger
from mower.utils.rapidocr import ocr_rec
from mower.utils.recognize import Scene
from mower.utils.solver import BaseSolver
from mower.utils.solver import BaseSolver, TransitionOn
from mower.utils.vector import va
from .utils import (
ClueMainSolver,
clue_cls,
clue_scope,
exit_pos,
is_orange,
main_dots,
main_scope,
@ -27,8 +27,10 @@ filter_receive = (1900, 45)
filter_self = (1610, 70)
class PlaceSolver(BaseSolver, BaseMixin):
class PlaceSolver(BaseSolver):
solver_name = "放置线索"
solver_max_duration = timedelta(minutes=2)
solver_default_scene = None
def run(self) -> None:
self.clue_status = {}
@ -49,12 +51,8 @@ class PlaceSolver(BaseSolver, BaseMixin):
return cl, st
return None, None
def transition(self) -> bool:
if (
scene := self.scene()
) == Scene.INFRA_DETAILS and self.detect_room() == "meeting":
self.tap((500, 1000))
elif scene == Scene.INFRA_CONFIDENTIAL:
@TransitionOn(Scene.INFRA_CONFIDENTIAL)
def _(self):
if self.animation():
return
if unlock_pos := self.detect_unlock():
@ -75,17 +73,16 @@ class PlaceSolver(BaseSolver, BaseMixin):
if st in ["available", "self", "available_self_only"]:
self.ctap(main_scope[cl], 1)
return
else:
return True
elif scene == Scene.CLUE_PLACE:
@TransitionOn(Scene.CLUE_PLACE)
def _(self):
cl, st = self.place_index()
if cl is None:
if unlock_pos := self.detect_unlock():
self.tap(unlock_pos)
return
else:
self.tap(exit_pos)
return True
if self.get_color((1328 + 77 * cl, 114))[0] < 150:
# 右上角 1-7
@ -114,9 +111,7 @@ class PlaceSolver(BaseSolver, BaseMixin):
time = ocr_rec(time_img)
else:
time = None
self.clue_list.append(
{"name": name, "time": time, "scope": tl2p(cp)}
)
self.clue_list.append({"name": name, "time": time, "scope": tl2p(cp)})
else:
break
if self.clue_list:
@ -146,7 +141,11 @@ class PlaceSolver(BaseSolver, BaseMixin):
self.clue_status[cl] = "self_only"
else:
self.clue_status[cl] = None
elif scene in self.waiting_scene:
@TransitionOn(BaseSolver.waiting_scene)
def _(self):
self.waiting_solver()
else:
EnterRoomSolver().run("meeting", detail=False)
@TransitionOn()
def _(self):
ClueMainSolver().run()

View file

@ -1,45 +1,44 @@
from mower.solvers.infra.base_mixin import BaseMixin
from mower.solvers.infra.enter_room import EnterRoomSolver
from datetime import timedelta
from mower.utils import config
from mower.utils.image import cropimg
from mower.utils.log import logger
from mower.utils.rapidocr import ocr_rec
from mower.utils.recognize import Scene
from mower.utils.solver import BaseSolver
from mower.utils.solver import BaseSolver, TransitionOn
from .utils import clue_cls, exit_pos
from .utils import ClueMainSolver, clue_cls
class ReceiveSolver(BaseSolver, BaseMixin):
class ReceiveSolver(BaseSolver):
solver_name = "接收好友线索"
solver_max_duration = timedelta(minutes=2)
solver_default_scene = None
def run(self):
super().run()
def transition(self) -> bool:
if (
scene := self.scene()
) == Scene.INFRA_DETAILS and self.detect_room() == "meeting":
self.tap((500, 1000))
elif scene == Scene.INFRA_CONFIDENTIAL:
@TransitionOn(Scene.INFRA_CONFIDENTIAL)
def _(self):
receive_scope = ((1822, 365), (1886, 400))
if self.find("clue/badge_new", scope=receive_scope):
self.ctap((1800, 430))
else:
return True
elif scene == Scene.CLUE_RECEIVE:
if self.find("infra_complete/信用", scope=((1440, 130), (1920, 193))):
self.sleep()
return
if clue := clue_cls("receive"):
return True
@TransitionOn(Scene.CLUE_RECEIVE)
def _(self):
if self.find("product_complete"):
return
if (clue := clue_cls("receive")) is None:
return True
name_scope = ((1580, 220), (1880, 255))
name_img = cropimg(config.recog.gray, name_scope)
name = ocr_rec(name_img) or "好友"
logger.info(f"接收{name}{clue}号线索")
self.tap(name_scope)
else:
self.tap(exit_pos)
elif scene in self.waiting_scene:
@TransitionOn(BaseSolver.waiting_scene)
def _(self):
self.waiting_solver()
else:
EnterRoomSolver().run("meeting", detail=False)
@TransitionOn()
def _(self):
ClueMainSolver().run()

View file

@ -1,7 +1,15 @@
import cv2
from datetime import timedelta
import cv2
import networkx as nx
from mower.solvers.infra.base_mixin import BaseMixin
from mower.solvers.infra.enter_room import EnterRoomSolver
from mower.utils import config
from mower.utils.graph.utils import DG
from mower.utils.image import cropimg, loadres
from mower.utils.recognize import Scene
from mower.utils.solver import BaseSolver, TransitionOn
from mower.utils.vector import va
clue_size = (162, 216)
@ -45,7 +53,6 @@ for i in range(1, 8):
main_time[i] = va(clue_top_left[i], main_time_offset)
main_scope[i] = tl2p(va(clue_top_left[i], main_offset))
tm_thres = 0.6
exit_pos = (1239, 144)
def clue_cls(scope):
@ -58,3 +65,53 @@ def clue_cls(scope):
if max_val > tm_thres:
return i
return None
class MeetingSolver(BaseSolver, BaseMixin):
solver_name = "会客室"
solver_max_duration = timedelta(minutes=2)
solver_default_scene = None
@TransitionOn(Scene.INFRA_MAIN)
def _(self):
EnterRoomSolver().run("meeting", detail=False)
@TransitionOn(Scene.INFRA_DETAILS)
def _(self):
if self.detect_room() != "meeting":
self.scene_graph_step(Scene.INFRA_MAIN)
return
if self.find("meeting_arrange_check_in"):
return True
self.tap((960, 540))
@TransitionOn()
def _(self):
try:
nx.shortest_path(DG, self.scene(), Scene.INFRA_DETAILS)
self.scene_graph_step(Scene.INFRA_DETAILS)
except nx.NetworkXNoPath:
self.scene_graph_step(Scene.INFRA_MAIN)
class ClueMainSolver(BaseSolver):
solver_name = "主界面"
solver_max_duration = timedelta(minutes=2)
solver_default_scene = None
@TransitionOn(Scene.INFRA_DETAILS)
def _(self):
MeetingSolver().run()
self.tap((500, 1000))
@TransitionOn(Scene.INFRA_CONFIDENTIAL)
def _(self):
return True
@TransitionOn()
def _(self):
try:
nx.shortest_path(DG, self.scene(), Scene.INFRA_CONFIDENTIAL)
self.scene_graph_step(Scene.INFRA_CONFIDENTIAL)
except nx.NetworkXNoPath:
self.scene_graph_step(Scene.INFRA_MAIN)

View file

@ -18,10 +18,10 @@ def todo_complete(solver: BaseSolver):
@edge(Scene.ACTIVITY_ROOM_DETAILS, Scene.INFRA_MAIN)
@edge(Scene.CTRLCENTER_ASSISTANT, Scene.INFRA_MAIN)
@edge(Scene.CLUE_DAILY, Scene.INFRA_CONFIDENTIAL)
@edge(Scene.CLUE_RECEIVE, Scene.INFRA_CONFIDENTIAL)
@edge(Scene.CLUE_RECEIVE, Scene.INFRA_DETAILS)
@edge(Scene.CLUE_GIVE_AWAY, Scene.INFRA_CONFIDENTIAL)
@edge(Scene.CLUE_SUMMARY, Scene.INFRA_CONFIDENTIAL)
@edge(Scene.CLUE_PLACE, Scene.INFRA_CONFIDENTIAL)
@edge(Scene.CLUE_PLACE, Scene.INFRA_DETAILS)
@edge(Scene.CLUE_MESSAGE_BOARD, Scene.INFRA_CONFIDENTIAL)
@edge(Scene.CLUE_MESSAGE_BOARD_FRIEND, Scene.CLUE_MESSAGE_BOARD)
@edge(Scene.CLUE_ACCESS_RECORD, Scene.CLUE_MESSAGE_BOARD)
@ -33,6 +33,12 @@ def infra_back(solver: BaseSolver):
solver.cback(3, id="infra_back")
@edge(Scene.CLUE_RECEIVE, Scene.INFRA_CONFIDENTIAL)
@edge(Scene.CLUE_PLACE, Scene.INFRA_CONFIDENTIAL)
def clue_receive_back(solver: BaseSolver):
solver.tap((960, 1000))
@edge(Scene.RIIC_OPERATOR_SELECT, Scene.INFRA_DETAILS)
def riic_operator_select(solver: BaseSolver):
solver.ctap("confirm_blue", 3)

View file

@ -283,7 +283,7 @@ class Recognizer:
self.scene = Scene.CLUE_GIVE_AWAY
elif self.find("clue/summary"):
self.scene = Scene.CLUE_SUMMARY
elif self.find("clue/filter_all"):
elif self.find("clue/filter_all") or self.find("clue/filter_all_on"):
self.scene = Scene.CLUE_PLACE
elif self.find("clue/message_board_banner"):
self.scene = Scene.CLUE_MESSAGE_BOARD

View file

@ -18,6 +18,7 @@ color = {
"clue/access_records.jpg": (843, 279),
"clue/daily": (526, 623),
"clue/filter_all": (1297, 99),
"clue/filter_all_on": (1299, 100),
"clue/give_away": (25, 18),
"clue/message_board_banner": (19, 1006),
"clue/message_board_collect": (1657, 876),

View file

@ -69,6 +69,7 @@ Res = Literal[
"clue/button_unlock",
"clue/daily",
"clue/filter_all",
"clue/filter_all_on",
"clue/give_away",
"clue/give_away_digit_bg",
"clue/icon_notification",