kotonebot 0.3.1__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. kotonebot/__init__.py +39 -39
  2. kotonebot/backend/bot.py +312 -302
  3. kotonebot/backend/color.py +525 -525
  4. kotonebot/backend/context/__init__.py +3 -3
  5. kotonebot/backend/context/context.py +49 -56
  6. kotonebot/backend/context/task_action.py +183 -175
  7. kotonebot/backend/core.py +129 -126
  8. kotonebot/backend/debug/entry.py +89 -89
  9. kotonebot/backend/debug/mock.py +78 -78
  10. kotonebot/backend/debug/server.py +222 -222
  11. kotonebot/backend/debug/vars.py +351 -351
  12. kotonebot/backend/dispatch.py +227 -227
  13. kotonebot/backend/flow_controller.py +196 -196
  14. kotonebot/backend/loop.py +12 -88
  15. kotonebot/backend/ocr.py +535 -529
  16. kotonebot/backend/preprocessor.py +103 -103
  17. kotonebot/client/__init__.py +9 -9
  18. kotonebot/client/device.py +528 -502
  19. kotonebot/client/fast_screenshot.py +377 -377
  20. kotonebot/client/host/__init__.py +43 -12
  21. kotonebot/client/host/adb_common.py +107 -94
  22. kotonebot/client/host/custom.py +118 -114
  23. kotonebot/client/host/leidian_host.py +196 -201
  24. kotonebot/client/host/mumu12_host.py +353 -358
  25. kotonebot/client/host/protocol.py +214 -213
  26. kotonebot/client/host/windows_common.py +58 -55
  27. kotonebot/client/implements/__init__.py +71 -7
  28. kotonebot/client/implements/adb.py +89 -85
  29. kotonebot/client/implements/adb_raw.py +162 -158
  30. kotonebot/client/implements/nemu_ipc/__init__.py +11 -7
  31. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
  32. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
  33. kotonebot/client/implements/remote_windows.py +188 -192
  34. kotonebot/client/implements/uiautomator2.py +85 -81
  35. kotonebot/client/implements/windows.py +176 -168
  36. kotonebot/client/protocol.py +69 -69
  37. kotonebot/client/registration.py +24 -24
  38. kotonebot/config/base_config.py +96 -96
  39. kotonebot/config/manager.py +36 -36
  40. kotonebot/errors.py +76 -71
  41. kotonebot/interop/win/__init__.py +10 -0
  42. kotonebot/interop/win/_mouse.py +311 -0
  43. kotonebot/interop/win/message_box.py +313 -313
  44. kotonebot/interop/win/reg.py +37 -37
  45. kotonebot/interop/win/shortcut.py +43 -43
  46. kotonebot/interop/win/task_dialog.py +513 -469
  47. kotonebot/logging/__init__.py +2 -2
  48. kotonebot/logging/log.py +17 -17
  49. kotonebot/primitives/__init__.py +17 -17
  50. kotonebot/primitives/geometry.py +862 -290
  51. kotonebot/primitives/visual.py +63 -63
  52. kotonebot/tools/mirror.py +354 -354
  53. kotonebot/ui/file_host/sensio.py +36 -36
  54. kotonebot/ui/file_host/tmp_send.py +54 -54
  55. kotonebot/ui/pushkit/__init__.py +3 -3
  56. kotonebot/ui/pushkit/image_host.py +88 -87
  57. kotonebot/ui/pushkit/protocol.py +13 -13
  58. kotonebot/ui/pushkit/wxpusher.py +54 -53
  59. kotonebot/ui/user.py +148 -143
  60. kotonebot/util.py +436 -409
  61. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/METADATA +82 -76
  62. kotonebot-0.5.0.dist-info/RECORD +71 -0
  63. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/licenses/LICENSE +673 -673
  64. kotonebot-0.3.1.dist-info/RECORD +0 -70
  65. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/WHEEL +0 -0
  66. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/top_level.txt +0 -0
kotonebot/backend/core.py CHANGED
@@ -1,126 +1,129 @@
1
- import logging
2
- from functools import cache
3
- from typing import Callable
4
-
5
- import cv2
6
- from cv2.typing import MatLike
7
-
8
- from kotonebot.util import cv2_imread
9
- from kotonebot.primitives import RectTuple, Rect, Point
10
- from kotonebot.errors import ResourceFileMissingError
11
-
12
- class Ocr:
13
- def __init__(
14
- self,
15
- text: str | Callable[[str], bool],
16
- *,
17
- language: str = 'jp',
18
- ):
19
- self.text = text
20
- self.language = language
21
-
22
-
23
- class Image:
24
- def __init__(
25
- self,
26
- *,
27
- path: str | None = None,
28
- name: str | None = 'untitled',
29
- data: MatLike | None = None,
30
- ):
31
- self.path = path
32
- self.name = name
33
- self.__data: MatLike | None = data
34
- self.__data_with_alpha: MatLike | None = None
35
-
36
- @cache
37
- def binary(self) -> 'Image':
38
- return Image(data=cv2.cvtColor(self.data, cv2.COLOR_BGR2GRAY))
39
-
40
- @property
41
- def data(self) -> MatLike:
42
- if self.__data is None:
43
- if self.path is None:
44
- raise ValueError('Either path or data must be provided.')
45
- self.__data = cv2_imread(self.path)
46
- if self.__data is None:
47
- raise ResourceFileMissingError(self.path, 'sprite')
48
- logger.debug(f'Read image "{self.name}" from {self.path}')
49
- return self.__data
50
-
51
- @property
52
- def data_with_alpha(self) -> MatLike:
53
- if self.__data_with_alpha is None:
54
- if self.path is None:
55
- raise ValueError('Either path or data must be provided.')
56
- self.__data_with_alpha = cv2_imread(self.path, cv2.IMREAD_UNCHANGED)
57
- if self.__data_with_alpha is None:
58
- raise ResourceFileMissingError(self.path, 'sprite with alpha')
59
- logger.debug(f'Read image "{self.name}" from {self.path}')
60
- return self.__data_with_alpha
61
-
62
- def __repr__(self) -> str:
63
- if self.path is None:
64
- return f'<Image: memory>'
65
- else:
66
- return f'<Image: "{self.name}" at {self.path}>'
67
-
68
-
69
- class HintBox(Rect):
70
- def __init__(
71
- self,
72
- x1: int,
73
- y1: int,
74
- x2: int,
75
- y2: int,
76
- *,
77
- name: str | None = None,
78
- description: str | None = None,
79
- source_resolution: tuple[int, int],
80
- ):
81
- super().__init__(x1, y1, x2 - x1, y2 - y1, name=name)
82
- self.description = description
83
- self.source_resolution = source_resolution
84
-
85
- @property
86
- def width(self) -> int:
87
- return self.x2 - self.x1
88
-
89
- @property
90
- def height(self) -> int:
91
- return self.y2 - self.y1
92
-
93
- @property
94
- def rect(self) -> RectTuple:
95
- return self.x1, self.y1, self.width, self.height
96
-
97
- class HintPoint(Point):
98
- def __init__(self, x: int, y: int, *, name: str | None = None, description: str | None = None):
99
- super().__init__(x, y, name=name)
100
- self.description = description
101
-
102
- def __repr__(self) -> str:
103
- return f'HintPoint<"{self.name}" at ({self.x}, {self.y})>'
104
-
105
- def unify_image(image: MatLike | str | Image, transparent: bool = False) -> MatLike:
106
- if isinstance(image, str):
107
- if not transparent:
108
- image = cv2_imread(image)
109
- else:
110
- image = cv2_imread(image, cv2.IMREAD_UNCHANGED)
111
- elif isinstance(image, Image):
112
- if transparent:
113
- image = image.data_with_alpha
114
- else:
115
- image = image.data
116
- return image
117
-
118
- logger = logging.getLogger(__name__)
119
-
120
-
121
- if __name__ == '__main__':
122
- hint_box = HintBox(100, 100, 200, 200, source_resolution=(1920, 1080))
123
- print(hint_box.rect)
124
- print(hint_box.width)
125
- print(hint_box.height)
126
-
1
+ import logging
2
+ from functools import cache
3
+ from typing import Callable
4
+ from typing_extensions import deprecated
5
+
6
+ import cv2
7
+ from cv2.typing import MatLike
8
+
9
+ from kotonebot.util import cv2_imread
10
+ from kotonebot.primitives import RectTuple, Rect, Point
11
+ from kotonebot.errors import ResourceFileMissingError
12
+
13
+ @deprecated('unused')
14
+ class Ocr:
15
+ def __init__(
16
+ self,
17
+ text: str | Callable[[str], bool],
18
+ *,
19
+ language: str = 'jp',
20
+ ):
21
+ self.text = text
22
+ self.language = language
23
+
24
+ # TODO: 这个类和 kotonebot.primitives.Image 重复了
25
+ @deprecated('Use kotonebot.primitives.Image instead.')
26
+ class Image:
27
+ def __init__(
28
+ self,
29
+ *,
30
+ path: str | None = None,
31
+ name: str | None = 'untitled',
32
+ data: MatLike | None = None,
33
+ ):
34
+ self.path = path
35
+ self.name = name
36
+ self.__data: MatLike | None = data
37
+ self.__data_with_alpha: MatLike | None = None
38
+
39
+ @cache
40
+ def binary(self) -> 'Image':
41
+ return Image(data=cv2.cvtColor(self.data, cv2.COLOR_BGR2GRAY))
42
+
43
+ @property
44
+ def data(self) -> MatLike:
45
+ if self.__data is None:
46
+ if self.path is None:
47
+ raise ValueError('Either path or data must be provided.')
48
+ self.__data = cv2_imread(self.path)
49
+ if self.__data is None:
50
+ raise ResourceFileMissingError(self.path, 'sprite')
51
+ logger.debug(f'Read image "{self.name}" from {self.path}')
52
+ return self.__data
53
+
54
+ @property
55
+ def data_with_alpha(self) -> MatLike:
56
+ if self.__data_with_alpha is None:
57
+ if self.path is None:
58
+ raise ValueError('Either path or data must be provided.')
59
+ self.__data_with_alpha = cv2_imread(self.path, cv2.IMREAD_UNCHANGED)
60
+ if self.__data_with_alpha is None:
61
+ raise ResourceFileMissingError(self.path, 'sprite with alpha')
62
+ logger.debug(f'Read image "{self.name}" from {self.path}')
63
+ return self.__data_with_alpha
64
+
65
+ def __repr__(self) -> str:
66
+ if self.path is None:
67
+ return f'<Image: memory>'
68
+ else:
69
+ return f'<Image: "{self.name}" at {self.path}>'
70
+
71
+ # TODO: 这里的其他类应该移动到 primitives 模块下面
72
+ class HintBox(Rect):
73
+ def __init__(
74
+ self,
75
+ x1: int,
76
+ y1: int,
77
+ x2: int,
78
+ y2: int,
79
+ *,
80
+ name: str | None = None,
81
+ description: str | None = None,
82
+ source_resolution: tuple[int, int],
83
+ ):
84
+ super().__init__(x1, y1, x2 - x1, y2 - y1, name=name)
85
+ self.description = description
86
+ self.source_resolution = source_resolution
87
+
88
+ @property
89
+ def width(self) -> int:
90
+ return self.x2 - self.x1
91
+
92
+ @property
93
+ def height(self) -> int:
94
+ return self.y2 - self.y1
95
+
96
+ @property
97
+ def rect(self) -> RectTuple:
98
+ return self.x1, self.y1, self.width, self.height
99
+
100
+ class HintPoint(Point):
101
+ def __init__(self, x: int, y: int, *, name: str | None = None, description: str | None = None):
102
+ super().__init__(x, y, name=name)
103
+ self.description = description
104
+
105
+ def __repr__(self) -> str:
106
+ return f'HintPoint<"{self.name}" at ({self.x}, {self.y})>'
107
+
108
+ def unify_image(image: MatLike | str | Image, transparent: bool = False) -> MatLike:
109
+ if isinstance(image, str):
110
+ if not transparent:
111
+ image = cv2_imread(image)
112
+ else:
113
+ image = cv2_imread(image, cv2.IMREAD_UNCHANGED)
114
+ elif isinstance(image, Image):
115
+ if transparent:
116
+ image = image.data_with_alpha
117
+ else:
118
+ image = image.data
119
+ return image
120
+
121
+ logger = logging.getLogger(__name__)
122
+
123
+
124
+ if __name__ == '__main__':
125
+ hint_box = HintBox(100, 100, 200, 200, source_resolution=(1920, 1080))
126
+ print(hint_box.rect)
127
+ print(hint_box.width)
128
+ print(hint_box.height)
129
+
@@ -1,89 +1,89 @@
1
- import os
2
- import runpy
3
- import shutil
4
- import argparse
5
- import importlib
6
- from pathlib import Path
7
- from threading import Thread
8
-
9
- from . import debug
10
- from kotonebot import logging
11
- from kotonebot.backend.context import init_context
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
- def _task_thread(task_module: str):
16
- """任务线程。"""
17
- runpy.run_module(task_module, run_name="__main__")
18
-
19
- def _parse_args():
20
- """解析命令行参数。"""
21
- parser = argparse.ArgumentParser(description='KotoneBot visual debug tool')
22
- parser.add_argument(
23
- '-s', '--save',
24
- help='Save dump image and results to the specified folder',
25
- type=str,
26
- metavar='PATH'
27
- )
28
- parser.add_argument(
29
- '-c', '--clear',
30
- help='Clear the dump folder before running',
31
- action='store_true'
32
- )
33
- parser.add_argument(
34
- '-t', '--config-type',
35
- help='The full path of the config data type. e.g. `kotonebot.tasks.common.BaseConfig`',
36
- type=str,
37
- metavar='TYPE',
38
- required=True
39
- )
40
- parser.add_argument(
41
- 'input_module',
42
- help='The module to run'
43
- )
44
- return parser.parse_args()
45
-
46
- def _start_task_thread(module: str):
47
- """启动任务线程。"""
48
- thread = Thread(target=_task_thread, args=(module,))
49
- thread.start()
50
-
51
- if __name__ == "__main__":
52
- args = _parse_args()
53
- debug.enabled = True
54
-
55
- # 设置保存路径
56
- if args.save:
57
- save_path = Path(args.save)
58
- debug.auto_save_to_folder = str(save_path)
59
- if not os.path.exists(save_path):
60
- os.makedirs(save_path)
61
- if args.clear:
62
- if debug.auto_save_to_folder:
63
- try:
64
- logger.info(f"Removing {debug.auto_save_to_folder}")
65
- shutil.rmtree(debug.auto_save_to_folder)
66
- except PermissionError:
67
- logger.warning(f"Failed to remove {debug.auto_save_to_folder}. Trying to remove all contents instead.")
68
- for root, dirs, files in os.walk(debug.auto_save_to_folder):
69
- for file in files:
70
- try:
71
- os.remove(os.path.join(root, file))
72
- except PermissionError:
73
- raise
74
-
75
-
76
- # 初始化上下文
77
- module_name, class_name = args.config_type.rsplit('.', 1)
78
- class_ = importlib.import_module(module_name).__getattribute__(class_name)
79
- init_context(config_type=class_)
80
-
81
- # 启动服务器
82
- from .server import app
83
- import uvicorn
84
-
85
- # 启动任务线程
86
- _start_task_thread(args.input_module)
87
-
88
- # 启动服务器
89
- uvicorn.run(app, host="127.0.0.1", port=8000, log_level='critical' if debug.hide_server_log else None)
1
+ import os
2
+ import runpy
3
+ import shutil
4
+ import argparse
5
+ import importlib
6
+ from pathlib import Path
7
+ from threading import Thread
8
+
9
+ from . import debug
10
+ from kotonebot import logging
11
+ from kotonebot.backend.context import init_context
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ def _task_thread(task_module: str):
16
+ """任务线程。"""
17
+ runpy.run_module(task_module, run_name="__main__")
18
+
19
+ def _parse_args():
20
+ """解析命令行参数。"""
21
+ parser = argparse.ArgumentParser(description='KotoneBot visual debug tool')
22
+ parser.add_argument(
23
+ '-s', '--save',
24
+ help='Save dump image and results to the specified folder',
25
+ type=str,
26
+ metavar='PATH'
27
+ )
28
+ parser.add_argument(
29
+ '-c', '--clear',
30
+ help='Clear the dump folder before running',
31
+ action='store_true'
32
+ )
33
+ parser.add_argument(
34
+ '-t', '--config-type',
35
+ help='The full path of the config data type. e.g. `kotonebot.tasks.common.BaseConfig`',
36
+ type=str,
37
+ metavar='TYPE',
38
+ required=True
39
+ )
40
+ parser.add_argument(
41
+ 'input_module',
42
+ help='The module to run'
43
+ )
44
+ return parser.parse_args()
45
+
46
+ def _start_task_thread(module: str):
47
+ """启动任务线程。"""
48
+ thread = Thread(target=_task_thread, args=(module,))
49
+ thread.start()
50
+
51
+ if __name__ == "__main__":
52
+ args = _parse_args()
53
+ debug.enabled = True
54
+
55
+ # 设置保存路径
56
+ if args.save:
57
+ save_path = Path(args.save)
58
+ debug.auto_save_to_folder = str(save_path)
59
+ if not os.path.exists(save_path):
60
+ os.makedirs(save_path)
61
+ if args.clear:
62
+ if debug.auto_save_to_folder:
63
+ try:
64
+ logger.info(f"Removing {debug.auto_save_to_folder}")
65
+ shutil.rmtree(debug.auto_save_to_folder)
66
+ except PermissionError:
67
+ logger.warning(f"Failed to remove {debug.auto_save_to_folder}. Trying to remove all contents instead.")
68
+ for root, dirs, files in os.walk(debug.auto_save_to_folder):
69
+ for file in files:
70
+ try:
71
+ os.remove(os.path.join(root, file))
72
+ except PermissionError:
73
+ raise
74
+
75
+
76
+ # 初始化上下文
77
+ module_name, class_name = args.config_type.rsplit('.', 1)
78
+ class_ = importlib.import_module(module_name).__getattribute__(class_name)
79
+ init_context(config_type=class_)
80
+
81
+ # 启动服务器
82
+ from .server import app
83
+ import uvicorn
84
+
85
+ # 启动任务线程
86
+ _start_task_thread(args.input_module)
87
+
88
+ # 启动服务器
89
+ uvicorn.run(app, host="127.0.0.1", port=8000, log_level='critical' if debug.hide_server_log else None)
@@ -1,79 +1,79 @@
1
- import time
2
- from typing_extensions import override
3
-
4
- import cv2
5
- from cv2.typing import MatLike
6
-
7
- from kotonebot import sleep
8
- from kotonebot.client.device import Device
9
-
10
- class Video:
11
- def __init__(self, path: str, fps: int):
12
- self.path = path
13
- self.fps = fps
14
- self.paused = False
15
- """是否暂停"""
16
- self.__cap = cv2.VideoCapture(path)
17
- self.__last_frame = None
18
- self.__last_time = 0
19
-
20
- def __iter__(self):
21
- return self
22
-
23
- def __next__(self):
24
- if self.paused:
25
- return self.__last_frame
26
- ret, frame = self.__cap.read()
27
- if not ret:
28
- raise StopIteration
29
- self.__last_frame = frame
30
- self.__last_time = time.time()
31
- if self.__last_time - time.time() < 1 / self.fps:
32
- sleep(1 / self.fps)
33
- return frame
34
-
35
- def pause(self):
36
- self.paused = True
37
-
38
- def resume(self):
39
- self.paused = False
40
-
41
- class MockDevice(Device):
42
- def __init__(
43
- self
44
- ):
45
- super().__init__()
46
- self.__video_stream = None
47
- self.__image = None
48
- self.__screen_size = None
49
-
50
- def load_video(self, path: str, fps: int):
51
- self.__video_stream = Video(path, fps)
52
- return self.__video_stream
53
-
54
- def load_image(self, img: str | MatLike):
55
- if isinstance(img, str):
56
- self.__image = cv2.imread(img)
57
- else:
58
- self.__image = img
59
- return self.__image
60
-
61
- def set_screen_size(self, width: int, height: int):
62
- self.__screen_size = (width, height)
63
-
64
- @override
65
- def screenshot(self):
66
- if self.__image is not None:
67
- return self.__image
68
- elif self.__video_stream is not None:
69
- return next(self.__video_stream)
70
- else:
71
- raise RuntimeError('No video stream loaded')
72
-
73
- @property
74
- @override
75
- def screen_size(self):
76
- if self.__screen_size is not None:
77
- return self.__screen_size
78
- else:
1
+ import time
2
+ from typing_extensions import override
3
+
4
+ import cv2
5
+ from cv2.typing import MatLike
6
+
7
+ from kotonebot import sleep
8
+ from kotonebot.client.device import Device
9
+
10
+ class Video:
11
+ def __init__(self, path: str, fps: int):
12
+ self.path = path
13
+ self.fps = fps
14
+ self.paused = False
15
+ """是否暂停"""
16
+ self.__cap = cv2.VideoCapture(path)
17
+ self.__last_frame = None
18
+ self.__last_time = 0
19
+
20
+ def __iter__(self):
21
+ return self
22
+
23
+ def __next__(self):
24
+ if self.paused:
25
+ return self.__last_frame
26
+ ret, frame = self.__cap.read()
27
+ if not ret:
28
+ raise StopIteration
29
+ self.__last_frame = frame
30
+ self.__last_time = time.time()
31
+ if self.__last_time - time.time() < 1 / self.fps:
32
+ sleep(1 / self.fps)
33
+ return frame
34
+
35
+ def pause(self):
36
+ self.paused = True
37
+
38
+ def resume(self):
39
+ self.paused = False
40
+
41
+ class MockDevice(Device):
42
+ def __init__(
43
+ self
44
+ ):
45
+ super().__init__()
46
+ self.__video_stream = None
47
+ self.__image = None
48
+ self.__screen_size = None
49
+
50
+ def load_video(self, path: str, fps: int):
51
+ self.__video_stream = Video(path, fps)
52
+ return self.__video_stream
53
+
54
+ def load_image(self, img: str | MatLike):
55
+ if isinstance(img, str):
56
+ self.__image = cv2.imread(img)
57
+ else:
58
+ self.__image = img
59
+ return self.__image
60
+
61
+ def set_screen_size(self, width: int, height: int):
62
+ self.__screen_size = (width, height)
63
+
64
+ @override
65
+ def screenshot(self):
66
+ if self.__image is not None:
67
+ return self.__image
68
+ elif self.__video_stream is not None:
69
+ return next(self.__video_stream)
70
+ else:
71
+ raise RuntimeError('No video stream loaded')
72
+
73
+ @property
74
+ @override
75
+ def screen_size(self):
76
+ if self.__screen_size is not None:
77
+ return self.__screen_size
78
+ else:
79
79
  raise RuntimeError('No screen size set')