ksaa 2025.6.8.0__py3-none-any.whl → 2025.6.28.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.
- kotonebot/backend/context/context.py +29 -25
- kotonebot/backend/debug/server.py +0 -2
- kotonebot/backend/dispatch.py +1 -17
- kotonebot/backend/loop.py +277 -0
- kotonebot/client/__init__.py +4 -2
- kotonebot/client/device.py +178 -25
- kotonebot/client/host/__init__.py +2 -1
- kotonebot/client/host/adb_common.py +94 -0
- kotonebot/client/host/custom.py +33 -4
- kotonebot/client/host/leidian_host.py +17 -15
- kotonebot/client/host/mumu12_host.py +79 -11
- kotonebot/client/host/protocol.py +44 -19
- kotonebot/client/host/windows_common.py +55 -0
- kotonebot/client/implements/__init__.py +7 -0
- kotonebot/client/implements/adb.py +24 -7
- kotonebot/client/implements/adb_raw.py +3 -3
- kotonebot/client/implements/nemu_ipc/__init__.py +8 -0
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +280 -0
- kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -0
- kotonebot/client/implements/remote_windows.py +17 -21
- kotonebot/client/implements/uiautomator2.py +5 -8
- kotonebot/client/implements/windows.py +28 -36
- kotonebot/client/protocol.py +13 -0
- kotonebot/client/registration.py +24 -0
- kotonebot/config/base_config.py +8 -2
- kotonebot/debug_entry.py +11 -4
- kotonebot/errors.py +7 -0
- kotonebot/kaa/game_ui/common.py +4 -7
- kotonebot/kaa/game_ui/dialog.py +24 -4
- kotonebot/kaa/game_ui/idols_overview.py +28 -12
- kotonebot/kaa/game_ui/primary_button.py +55 -0
- kotonebot/kaa/game_ui/scrollable.py +0 -5
- kotonebot/kaa/main/cli.py +9 -4
- kotonebot/kaa/main/dmm_host.py +15 -8
- kotonebot/kaa/main/gr.py +114 -27
- kotonebot/kaa/main/kaa.py +140 -41
- kotonebot/kaa/metadata.py +47 -0
- kotonebot/kaa/resources/__pycache__/__init__.cpython-310.pyc +0 -0
- kotonebot/kaa/resources/game.db +0 -0
- kotonebot/kaa/resources/game_ver.txt +0 -0
- kotonebot/kaa/resources/idol_cards/i_card-skin-kllj-3-012_0.png +0 -0
- kotonebot/kaa/resources/idol_cards/i_card-skin-kllj-3-012_1.png +0 -0
- kotonebot/kaa/resources/idol_cards/i_card-skin-ssmk-3-012_0.png +0 -0
- kotonebot/kaa/resources/idol_cards/i_card-skin-ssmk-3-012_1.png +0 -0
- kotonebot/kaa/sprites/24e99232-9434-457f-a9a0-69dd7ecf675f.png +0 -0
- kotonebot/kaa/sprites/2ededcf5-1d80-4e2a-9c83-2a31998331ce.png +0 -0
- kotonebot/kaa/sprites/3b473fe6-e147-477f-b088-9b8fb042a4f6.png +0 -0
- kotonebot/kaa/sprites/55f7db71-0a18-4b3d-b847-57959b8d2e32.png +0 -0
- kotonebot/kaa/sprites/6cd80be8-c9b3-4ba5-bf17-3ffc9b000743.png +0 -0
- kotonebot/kaa/sprites/74ec3510-583d-4a76-ac69-38480fbf1387.png +0 -0
- kotonebot/kaa/sprites/a0bd6a5f-784d-4f0a-9d66-10f4b80c8d3e.png +0 -0
- kotonebot/kaa/sprites/d3424d31-0502-4623-996e-f0194e5085ce.png +0 -0
- kotonebot/kaa/sprites/e6b45405-cd9f-4c6e-a9f1-6ec953747c65.png +0 -0
- kotonebot/kaa/sprites/f5c16d2f-ebc5-4617-9b96-971696af7c52.png +0 -0
- kotonebot/kaa/tasks/R.py +157 -135
- kotonebot/kaa/{clear_logs.py → tasks/clear_logs.py} +0 -3
- kotonebot/kaa/tasks/produce/in_purodyuusu.py +0 -4
- kotonebot/kaa/tasks/produce/non_lesson_actions.py +0 -3
- kotonebot/kaa/tasks/produce/produce.py +119 -70
- kotonebot/kaa/tasks/start_game.py +7 -6
- kotonebot/kaa/util/paths.py +6 -1
- kotonebot/tools/mirror.py +23 -13
- kotonebot/util.py +32 -0
- {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/METADATA +4 -1
- {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/RECORD +201 -181
- kotonebot/client/factory.py +0 -92
- kotonebot/kaa/sprites/38b8f119-8de1-49bf-8a5f-a44371de4569.png +0 -0
- /kotonebot/kaa/sprites/{5ca50448-cc21-499a-a685-c5e1c4240c48.png → 019273c4-22ce-486f-8a7d-dbc78f13dcac.png} +0 -0
- /kotonebot/kaa/sprites/{afa0f85f-dcb7-48a1-a1ae-624456b9b65c.png → 03e34ef7-36b5-4ddc-9545-5b70a2eba72e.png} +0 -0
- /kotonebot/kaa/sprites/{a00fcf77-2800-4f7d-993d-4636b36045bb.png → 06ea773b-e2b3-40d4-82f6-b119ce9ca00b.png} +0 -0
- /kotonebot/kaa/sprites/{d58d9987-b1ca-4669-a9a6-9a0fd1847fa6.png → 0769f8f4-1c47-4bad-a783-bba5e20250bf.png} +0 -0
- /kotonebot/kaa/sprites/{82a5e224-a161-43d4-8e15-2946480336dd.png → 08946997-4e9e-4242-95e1-e988fa96b919.png} +0 -0
- /kotonebot/kaa/sprites/{fb927b92-7a82-4869-8c61-581d49cd7201.png → 0d60ce10-20ce-4980-8172-b6bac08d6258.png} +0 -0
- /kotonebot/kaa/sprites/{b1e88fda-434a-4d06-8891-883a7beafa12.png → 0f40352f-6443-406e-8e2c-c40d5cfc6c59.png} +0 -0
- /kotonebot/kaa/sprites/{c4f28132-4aba-40ba-be6c-b1a31d014635.png → 0f6d7f89-bdb2-4235-9b6b-e65cb73a4167.png} +0 -0
- /kotonebot/kaa/sprites/{e866d8b1-4f61-45bf-8170-5229f53c047a.png → 14d09c44-3897-4c81-b615-19c35d0f3793.png} +0 -0
- /kotonebot/kaa/sprites/{a105d17d-d44c-4798-aeff-70808c91cb47.png → 15191dec-7e33-4721-8068-15c11978e885.png} +0 -0
- /kotonebot/kaa/sprites/{869b1771-b2ce-4872-ab75-2157c47c5b13.png → 15f8f31e-65ce-408d-8556-b799bcefc9d4.png} +0 -0
- /kotonebot/kaa/sprites/{994aa6a6-0189-4a79-8125-56136973b581.png → 20b01af5-907b-4d49-9f5b-a3700ca9c0c3.png} +0 -0
- /kotonebot/kaa/sprites/{92b7d830-3db5-434e-b156-0ac855db480d.png → 242c088b-bc73-4d11-aa31-ee7c71477fb4.png} +0 -0
- /kotonebot/kaa/sprites/{9c1fa1d4-2ae5-4093-abaa-cebb51135721.png → 2f2a7951-a30a-4835-b9f6-3205cfdd2f05.png} +0 -0
- /kotonebot/kaa/sprites/{a33ae1c3-ac69-4783-977e-9f0f1b1a2f8d.png → 3031397e-3d12-4f23-8377-006eca5d2627.png} +0 -0
- /kotonebot/kaa/sprites/{e7022542-4317-4a61-8098-f3f733d8400e.png → 312dbd82-5457-474a-91ce-511f0e4e9c80.png} +0 -0
- /kotonebot/kaa/sprites/{cd8d3d55-f102-419e-a6fd-2ad5ecc4fd2e.png → 31851ad1-df39-4a4f-9ae7-19c1d1c6b51a.png} +0 -0
- /kotonebot/kaa/sprites/{826bf80a-112e-4356-9111-4b7e692ccc82.png → 338b699a-d556-4b80-a577-123442c3db45.png} +0 -0
- /kotonebot/kaa/sprites/{d8136469-6d73-4f82-a79c-fee83c3e93a8.png → 35319890-98e3-4f75-b969-6b8576ef668d.png} +0 -0
- /kotonebot/kaa/sprites/{96cf0633-6531-499b-b1cc-6670ab710293.png → 3536f829-70d3-48f9-8fd4-b65eab7502f3.png} +0 -0
- /kotonebot/kaa/sprites/{31a52dc8-b7fb-4ff6-a261-eb90a918603c.png → 35516b69-a4f4-4ad4-8af4-35043475ee90.png} +0 -0
- /kotonebot/kaa/sprites/{d747239f-7620-4f23-9ac7-cf2d34889266.png → 36d71d33-09c9-4be8-b184-ccf40f700333.png} +0 -0
- /kotonebot/kaa/sprites/{b44f5e5b-e999-4427-8a9f-f76c6485f4a5.png → 379d5084-637f-45e8-8bd8-789eaa919821.png} +0 -0
- /kotonebot/kaa/sprites/{0a430873-8c12-4ce2-a5de-ab9790c08344.png → 3851303a-e3a0-46eb-aa7e-b47fb41946f7.png} +0 -0
- /kotonebot/kaa/sprites/{ff77d221-8444-4d0f-8c93-0223a97e2a98.png → 3d1aa368-e198-478f-a47f-49818efbaec8.png} +0 -0
- /kotonebot/kaa/sprites/{333fe6d3-0be7-40fb-a2a3-364f98758f2a.png → 3f5907a9-7fe4-4f46-8e46-a96ac7fa3923.png} +0 -0
- /kotonebot/kaa/sprites/{0decaa6a-b9a9-4d9b-829a-0fc1f67538b4.png → 40c0f296-d29e-4e05-aaff-a212339b742e.png} +0 -0
- /kotonebot/kaa/sprites/{c1ad996a-925e-41b5-b26c-d48b8ded21d2.png → 43e72796-a644-4d9b-a817-8add011d9a75.png} +0 -0
- /kotonebot/kaa/sprites/{e2fae2a4-bb19-4f49-b382-47e504ec26d3.png → 469c2b41-4171-4902-acc5-da0c807886ab.png} +0 -0
- /kotonebot/kaa/sprites/{559df9b9-e74d-45af-a300-77d3424e69c6.png → 497e7930-f423-457a-8168-b1a1532bb1e9.png} +0 -0
- /kotonebot/kaa/sprites/{e41bb5df-164d-4760-9db6-d5d5d763bb5f.png → 49a43af3-17e3-4434-ba4d-519775ccaea9.png} +0 -0
- /kotonebot/kaa/sprites/{f9b2f64d-f829-4b13-9704-c5f00fb956be.png → 49b9a298-e847-4322-99a2-c0588c3542fe.png} +0 -0
- /kotonebot/kaa/sprites/{645744c4-3bbf-4ea2-8f46-4725fff874b3.png → 4a1902aa-01db-46bb-bea5-88ac62c82562.png} +0 -0
- /kotonebot/kaa/sprites/{c0fc7de5-b842-4c70-9cf2-89d833bfe7a6.png → 4a96a728-6303-4ae3-8e0c-9f1e8e5f86a7.png} +0 -0
- /kotonebot/kaa/sprites/{73f4204e-8281-47cc-9dce-6bb8280c4ce5.png → 4b7c4221-a679-47f6-929d-0ef514894b27.png} +0 -0
- /kotonebot/kaa/sprites/{c1b1125f-542b-4e63-9c86-5d2e35a89a10.png → 4d355e36-9169-43e4-bd58-2836ee3e56e0.png} +0 -0
- /kotonebot/kaa/sprites/{ad8ee599-88a8-429c-8982-99086e552dcc.png → 4eb97d68-cd47-4100-9c3b-35b5c86b4bdf.png} +0 -0
- /kotonebot/kaa/sprites/{7a479871-547e-4098-9cdd-6c9bb9f7449a.png → 515a2ff0-36b8-4f2f-9902-e6c1b9796586.png} +0 -0
- /kotonebot/kaa/sprites/{02e0aa0d-4094-4bdc-8625-1a063733615b.png → 51a3ed29-92b9-4429-9e50-783d1b8dc6a7.png} +0 -0
- /kotonebot/kaa/sprites/{22d5a7d5-59a0-4137-b02e-1a7af86d4250.png → 52d3438b-afae-4c7a-abe5-7ed530c7558c.png} +0 -0
- /kotonebot/kaa/sprites/{5bf00cba-9aab-4ffe-b20b-8f5e74279276.png → 540ee3b5-ca8f-4091-80a6-ce774d95e5f7.png} +0 -0
- /kotonebot/kaa/sprites/{05ff5efd-0f76-47ce-bc89-237266bc17c0.png → 54a15298-ecaf-4cc5-b8a2-5b0d8f8add41.png} +0 -0
- /kotonebot/kaa/sprites/{1f9165e6-77ee-403e-9b76-99c114527dbb.png → 554c0ff6-0a05-419d-85f9-5ced260d0d2e.png} +0 -0
- /kotonebot/kaa/sprites/{79bc56c5-eae6-4389-9116-4a9e182d6bf3.png → 55e4ef75-ba8e-4230-819c-450cf14b08cd.png} +0 -0
- /kotonebot/kaa/sprites/{cd4cbd7b-df80-45bb-a053-cc333a806934.png → 5824ef5f-6776-402c-b78c-ae79cc4cc878.png} +0 -0
- /kotonebot/kaa/sprites/{804a4b05-1ca3-4915-bf23-a66ccdc34404.png → 5a393483-c631-47fd-805c-c460c23b8f9e.png} +0 -0
- /kotonebot/kaa/sprites/{e5d15c50-45ee-4de9-8b97-4630cbd8f01a.png → 5b75cae4-a853-402c-acec-edbfef94342a.png} +0 -0
- /kotonebot/kaa/sprites/{1a3d18a4-7d91-4182-9e40-e60d73fed365.png → 5c9ae62c-488f-4109-a4d2-d2385c32043d.png} +0 -0
- /kotonebot/kaa/sprites/{7344e180-251b-465a-a8cf-974477f7ad44.png → 5cdd71be-c8cd-4310-b6f4-5e97ddbeb46e.png} +0 -0
- /kotonebot/kaa/sprites/{67a81c23-4ce4-45e3-bfa8-1136fb14b831.png → 5d5cb13b-c1fd-4808-b352-b21291a9ff0b.png} +0 -0
- /kotonebot/kaa/sprites/{4e21f02d-d5e1-49eb-946a-8edc74d0f1f7.png → 5d6c4462-3d7c-42eb-ab3e-195ebf845a09.png} +0 -0
- /kotonebot/kaa/sprites/{d7f604a6-c663-44db-b1d3-c9792eb3daa4.png → 5d94e628-82ff-4531-b70a-b13297491bea.png} +0 -0
- /kotonebot/kaa/sprites/{74521b6f-de5e-4ba8-a3c9-c6ba01070498.png → 5dc2dca3-48bd-4eee-af89-1a1d30593bd5.png} +0 -0
- /kotonebot/kaa/sprites/{b604e25d-680a-4e02-a012-2e593b5bc276.png → 5f0c2a57-efd0-4094-9f71-6f6fe97316f6.png} +0 -0
- /kotonebot/kaa/sprites/{fd610de1-b023-4747-8c1b-72f89b1c5647.png → 610348f2-6262-4cd1-8fc3-e7470f85580c.png} +0 -0
- /kotonebot/kaa/sprites/{5be4f6c7-2baa-42f3-a5a5-99ca0830cc30.png → 613e9b10-b2eb-44ea-b6d9-9310c0e1bdcf.png} +0 -0
- /kotonebot/kaa/sprites/{72c1804f-db8c-40fa-bc79-6533f2861a5d.png → 618b21ad-ab8b-436a-8eb8-bd320a198c9f.png} +0 -0
- /kotonebot/kaa/sprites/{572077d4-7ee2-449a-b4f0-66239b0d7aff.png → 61a09df6-7443-436c-9ef0-1d5a1cae1509.png} +0 -0
- /kotonebot/kaa/sprites/{321b54c8-c69b-4437-931d-655882dabeed.png → 64be274e-e8ad-4938-84c1-5d8aabcd10d8.png} +0 -0
- /kotonebot/kaa/sprites/{22ee3f1b-ddcb-48bf-85dd-8423f85e3199.png → 671b4a55-023a-434d-b29a-e844fe528e6b.png} +0 -0
- /kotonebot/kaa/sprites/{668002b6-805b-467b-a703-203fb75444f4.png → 6e5e9cf9-80b0-4241-a222-ff50dbd89a0c.png} +0 -0
- /kotonebot/kaa/sprites/{4bd66b38-78d3-4a27-816b-d315039a8bb6.png → 6ef59649-bbb3-42b6-a6b0-eac4f35a3da4.png} +0 -0
- /kotonebot/kaa/sprites/{7b3c6860-fbbe-48aa-aadf-8062fa88f1fe.png → 75a2093d-8743-427e-8d24-ae9a69fa14c1.png} +0 -0
- /kotonebot/kaa/sprites/{371a3192-101c-45ef-9da4-2ed389770c0f.png → 76bde19a-4b1b-4e52-86d9-f56be7529c71.png} +0 -0
- /kotonebot/kaa/sprites/{ef39804e-8d2d-40f6-8d13-bd2877d1353a.png → 77d06f4d-8214-4306-9dc5-035c720f7256.png} +0 -0
- /kotonebot/kaa/sprites/{24cd42fd-bb3b-4fc6-b40d-d3e3e36fe9f1.png → 78b642d3-9a2c-4c17-8872-3cc78211f4a0.png} +0 -0
- /kotonebot/kaa/sprites/{0bab7080-9a37-4c27-9821-8df36d49b678.png → 7a9d1c39-b306-4cb0-be2f-7a5963099949.png} +0 -0
- /kotonebot/kaa/sprites/{b1d1dfab-51f5-4ae9-b718-cdf46774c0ff.png → 7ddda828-c8b0-4331-864f-99b85b7c5557.png} +0 -0
- /kotonebot/kaa/sprites/{5edef075-4843-4dc9-840f-ae445cbe0730.png → 81399fd9-f254-4d0e-9775-ad451c237a4e.png} +0 -0
- /kotonebot/kaa/sprites/{705d9ecf-faf8-40f4-b701-637cec822c1e.png → 81526e97-1b8d-433f-a5c9-907e4e8c85cd.png} +0 -0
- /kotonebot/kaa/sprites/{ecd44a4a-e1ca-4a86-ae0d-2306a672613f.png → 818e2370-8fb9-4e6c-ab78-58e86d6525b5.png} +0 -0
- /kotonebot/kaa/sprites/{4f58031c-36b9-4baf-bccf-11c05d83bd19.png → 8431cc52-0041-4425-808c-dcd4d196506c.png} +0 -0
- /kotonebot/kaa/sprites/{13535ecd-bdf5-4ff7-aa2c-6ecb20fe9c06.png → 85a49ff3-6075-4797-8b7f-35851a843698.png} +0 -0
- /kotonebot/kaa/sprites/{d22ca9e0-001d-43d6-a557-1c76c5a27c81.png → 86062e91-d6c1-44cf-9f8c-26a8b9dd8686.png} +0 -0
- /kotonebot/kaa/sprites/{0bbfb5b0-f0f7-4ad9-86f7-a2adb01a6371.png → 860c3d98-e263-482f-95fb-4fc9c472e8b8.png} +0 -0
- /kotonebot/kaa/sprites/{ed5d2bf3-405c-4165-88d0-9f8a4be968c6.png → 8669ce44-dec7-47c2-aafd-3f2fbfd1d083.png} +0 -0
- /kotonebot/kaa/sprites/{f0786a26-81b5-4e76-b577-db70ab3fab50.png → 86b15801-c932-4034-bed8-fd76b0f65916.png} +0 -0
- /kotonebot/kaa/sprites/{80fb515c-4323-422d-a24c-80a3bf702f1d.png → 893b015d-6894-4e74-880e-d22859a25662.png} +0 -0
- /kotonebot/kaa/sprites/{fdfecda8-2fe0-487e-b5fe-6358274bb9ff.png → 8c0fa5fa-cebb-4ae7-bd1c-b542c9cbf694.png} +0 -0
- /kotonebot/kaa/sprites/{73af305f-840f-4325-abae-b3519fba672a.png → 913a1d17-54fe-4c8f-ac50-ef7f39600b86.png} +0 -0
- /kotonebot/kaa/sprites/{c2667677-cbff-4dd4-a401-e0963e7224f4.png → 964c6b90-f526-4199-9523-d8124373a56b.png} +0 -0
- /kotonebot/kaa/sprites/{db94ffb7-a09d-4a51-b420-b930851c9db4.png → 97c281a9-ebb0-4f54-b644-b7ed7dd65f81.png} +0 -0
- /kotonebot/kaa/sprites/{48c257f8-3583-49f7-b01a-ba7d251086ac.png → 998fc84a-af71-4fa5-8aa4-e0bfeb9c53ea.png} +0 -0
- /kotonebot/kaa/sprites/{469006bc-a011-46e4-bcde-992be888456e.png → 9cbf7f6e-fe77-463b-9c60-495dc823510a.png} +0 -0
- /kotonebot/kaa/sprites/{4d6c198a-0e34-4c80-86d1-7789b2082d64.png → 9ce7d48e-295d-47b2-96eb-f3ce697c1b37.png} +0 -0
- /kotonebot/kaa/sprites/{a3c12040-2b11-4038-b830-215fb7b6f359.png → 9dcca89b-c92b-4708-8f19-facd40d09a65.png} +0 -0
- /kotonebot/kaa/sprites/{fbd816df-7ae6-40a4-b4d4-5708f5ba4a7c.png → 9e369992-ea8c-4c15-88d3-1344319978b0.png} +0 -0
- /kotonebot/kaa/sprites/{54bb5fcf-be61-4af8-bb19-a3c5113cf9e1.png → 9f9003ba-468f-406f-a82f-1b7a23ef347c.png} +0 -0
- /kotonebot/kaa/sprites/{309105d4-073a-47c9-8c5c-d593aa497723.png → a2bf4e11-fbc3-4172-bc0b-b16f8f59e68c.png} +0 -0
- /kotonebot/kaa/sprites/{111a03e6-e401-4ad5-b5dc-d5124e8f1ab0.png → a546e254-0490-43df-a291-a9a5c2b4c34a.png} +0 -0
- /kotonebot/kaa/sprites/{7736a171-9221-4cda-8288-908997abeff2.png → aacd229c-d307-4a20-a388-84d8f4afb4a5.png} +0 -0
- /kotonebot/kaa/sprites/{1bfce48a-b151-406e-a6f5-863cb68cbbf2.png → b003bef5-53db-4032-b1bd-d5075de365a8.png} +0 -0
- /kotonebot/kaa/sprites/{b2f7a430-5777-4861-8435-5419394e5ee8.png → b641a122-4734-43e1-b22f-468464a0cff1.png} +0 -0
- /kotonebot/kaa/sprites/{a82e06fb-a02e-4be5-a7b4-4d2758914fcf.png → bc7303f3-43e9-4ece-9206-5f9c779b813b.png} +0 -0
- /kotonebot/kaa/sprites/{f74fc514-61f1-4068-bdf4-754fcaf8ac89.png → bd7a6754-fffc-4315-987f-5ba32d95b697.png} +0 -0
- /kotonebot/kaa/sprites/{c7aa3f89-8a5e-4685-9297-4fc892bfdd81.png → c39f2006-442b-4ab7-99bb-3dfa0673f05d.png} +0 -0
- /kotonebot/kaa/sprites/{6bf5556a-8f4e-41d7-8ab5-d683ad1b47a8.png → c70f4634-6821-4480-a615-b5f9cdc578c1.png} +0 -0
- /kotonebot/kaa/sprites/{89c0e675-3fd4-456a-8ae7-3bfb248b7281.png → c774098e-d782-455c-80a9-5bad3713129d.png} +0 -0
- /kotonebot/kaa/sprites/{09433024-3dfe-4fcb-9d67-cf442b58f31b.png → c7e3736a-0e0f-474a-9386-f58292a45b92.png} +0 -0
- /kotonebot/kaa/sprites/{48d2f007-8c1b-43fc-bd3f-f0b10d2a0c88.png → c90b582c-8570-48b4-98c0-62d5b067ca2c.png} +0 -0
- /kotonebot/kaa/sprites/{9bb1a400-935a-4c5e-838b-fca502c49aba.png → c9d3699b-c8b1-4294-9b48-103df4ab6c65.png} +0 -0
- /kotonebot/kaa/sprites/{157dd58d-e319-4e0c-a37a-638e1a31c60b.png → cc9483c3-c895-4dcc-b3f2-0934ba1ad42e.png} +0 -0
- /kotonebot/kaa/sprites/{43d65b3f-bbae-4e29-a7bf-539379c1b072.png → cec49286-3d48-4fe0-8911-6a1b17d1ac20.png} +0 -0
- /kotonebot/kaa/sprites/{56a8c0ca-4659-4de2-9bce-55ce86a40d75.png → cef78712-d9ae-4d6e-8d7d-d047d48fbb98.png} +0 -0
- /kotonebot/kaa/sprites/{12e8d3f1-494a-46f5-908f-f621a0c57ec6.png → d06fd859-4678-4593-b096-c86b38810cb7.png} +0 -0
- /kotonebot/kaa/sprites/{ba955365-1328-4018-83a2-bfe818f8f172.png → d19c37ff-d5c1-4316-8424-e69ccb5c2523.png} +0 -0
- /kotonebot/kaa/sprites/{695f8d57-b2c9-4a6d-9ae9-1b49ee3eb22a.png → d26b60f7-d15c-4563-bedc-dd6e7e4078b2.png} +0 -0
- /kotonebot/kaa/sprites/{910d307b-6461-4950-badb-509975d0b797.png → d26bf52c-cb1b-4ebf-9c03-ff3f2d245959.png} +0 -0
- /kotonebot/kaa/sprites/{7e8164f6-a9e4-4600-b776-77b35a490933.png → d5fd6c73-4ad8-47b5-89be-c7b137a9950f.png} +0 -0
- /kotonebot/kaa/sprites/{05bfd235-2077-479a-8c43-11fb49316000.png → d696062b-9b58-4af1-835e-2cf5a0a0fbeb.png} +0 -0
- /kotonebot/kaa/sprites/{03458cf9-8ab0-4908-9a56-9c5c7e06bc9b.png → da7a329e-339e-4ee7-8775-bca505a1980d.png} +0 -0
- /kotonebot/kaa/sprites/{2f81e70a-f2af-420f-9b6b-6338c391911f.png → dedb8499-5881-4add-914f-ff0049285dae.png} +0 -0
- /kotonebot/kaa/sprites/{b3d1f8d4-1393-4206-b78e-a5495d8fb930.png → dedea21b-f21d-4621-a213-70ad03faec58.png} +0 -0
- /kotonebot/kaa/sprites/{f65a4495-f044-44de-bf0d-c869e637d5d3.png → dff74cce-ba2d-44aa-b924-183259080a32.png} +0 -0
- /kotonebot/kaa/sprites/{b3e72de2-e103-49a3-b0f2-8593cff87c4e.png → e0246b0d-f0cf-49d4-8423-7da46e75a575.png} +0 -0
- /kotonebot/kaa/sprites/{624555b9-31e3-46a3-8078-55619eaa40b9.png → e1b6b32b-c797-4f1d-aa2b-e7320083a9a6.png} +0 -0
- /kotonebot/kaa/sprites/{90a9c819-ee6b-4b3e-a0e4-ce484c6e413d.png → e23e3f19-03f6-4507-9128-115191272b90.png} +0 -0
- /kotonebot/kaa/sprites/{cc829886-23e8-4d47-b4c4-37ca20381180.png → e2eb3bae-6451-40b9-bbff-90f11dca1904.png} +0 -0
- /kotonebot/kaa/sprites/{6a7d061e-682b-42c1-97d5-121c52921e8b.png → e46e1b6d-6702-46bf-a07e-aec8d183453f.png} +0 -0
- /kotonebot/kaa/sprites/{de709296-e411-465f-86fc-5daf5ca83cef.png → e5297036-9934-44a4-a761-a6a49ae06e3e.png} +0 -0
- /kotonebot/kaa/sprites/{52620946-b92c-42c7-9fcc-0ff13388fd0d.png → e7da5988-0e83-44ad-888b-246365473f9e.png} +0 -0
- /kotonebot/kaa/sprites/{05f8f996-a38a-45d9-8ff9-720b6c4fd599.png → eb8d208a-dbcc-43ca-a8ac-098e1005ebfa.png} +0 -0
- /kotonebot/kaa/sprites/{8e8a0b57-0231-4ed6-958e-3196f7c9ca7b.png → ebe4ce64-98a9-40f2-9fde-3d5cdae673c7.png} +0 -0
- /kotonebot/kaa/sprites/{4a235eaa-46f1-4f71-b54d-ada0ade44572.png → ef2ad3df-3f89-4a48-8fc3-c70a8adc1044.png} +0 -0
- /kotonebot/kaa/sprites/{f8cbb089-a63e-4e40-9238-26d14da2c459.png → f05e410d-15b8-4831-bcc1-4cd8af07c882.png} +0 -0
- /kotonebot/kaa/sprites/{550fc45d-22c0-4a7c-a324-2c4ff107ab39.png → f1fac13a-84f3-42bd-9b52-e0ba90bc5255.png} +0 -0
- /kotonebot/kaa/sprites/{ad784a29-f0e2-4037-976b-45143db8441c.png → f7c34ab6-c165-48b7-ad5a-0b60613ce1bc.png} +0 -0
- /kotonebot/kaa/sprites/{0439399f-5fca-43f9-a105-99342ff04d2e.png → fa80b14e-953f-44bd-a4ad-99aefd276fd7.png} +0 -0
- /kotonebot/kaa/sprites/{7cea265a-fca5-43d0-b2da-067c8fff4d72.png → fe35655e-701b-4081-8df8-c17a7252acef.png} +0 -0
- /kotonebot/kaa/sprites/{da3e7cee-d77b-4e62-b703-ed257706dd39.png → fe853ac7-934e-4b4a-bcc7-b986e057dfcf.png} +0 -0
- /kotonebot/kaa/sprites/{1f8f8c46-43ec-4449-be9f-2e96da11da1e.png → ffda716c-2903-4ec7-8c59-aab5b8ad10bb.png} +0 -0
- /kotonebot/kaa/sprites/{4c72f39e-1182-4479-867f-8207744ee4fd.png → fff33200-c947-49f2-ab86-3ac98937fa36.png} +0 -0
- {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/WHEEL +0 -0
- {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/entry_points.txt +0 -0
- {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/licenses/LICENSE +0 -0
- {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import ctypes
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from time import sleep
|
|
7
|
+
from typing import Literal
|
|
8
|
+
from typing_extensions import override
|
|
9
|
+
|
|
10
|
+
import cv2
|
|
11
|
+
import numpy as np
|
|
12
|
+
from cv2.typing import MatLike
|
|
13
|
+
|
|
14
|
+
from ...device import AndroidDevice, Device
|
|
15
|
+
from ...protocol import Touchable, Screenshotable
|
|
16
|
+
from ...registration import ImplConfig
|
|
17
|
+
from .external_renderer_ipc import ExternalRendererIpc
|
|
18
|
+
from kotonebot.errors import KotonebotError
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NemuIpcIncompatible(Exception):
|
|
24
|
+
"""MuMu12 版本过低或 dll 不兼容"""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NemuIpcError(KotonebotError):
|
|
29
|
+
"""调用 IPC 过程中发生错误"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class NemuIpcImplConfig(ImplConfig):
|
|
35
|
+
"""nemu_ipc 能力的配置模型。"""
|
|
36
|
+
nemu_folder: str
|
|
37
|
+
r"""MuMu12 根目录(如 F:\Apps\Netease\MuMuPlayer-12.0)。"""
|
|
38
|
+
instance_id: int
|
|
39
|
+
"""模拟器实例 ID。"""
|
|
40
|
+
display_id: int | None = 0
|
|
41
|
+
"""目标显示器 ID,默认为 0(主显示器)。若为 None 且设置了 target_package_name,则自动获取对应的 display_id。"""
|
|
42
|
+
target_package_name: str | None = None
|
|
43
|
+
"""目标应用包名,用于自动获取 display_id。"""
|
|
44
|
+
app_index: int = 0
|
|
45
|
+
"""多开应用索引,传给 get_display_id 方法。"""
|
|
46
|
+
wait_package_timeout: float = 60 # 单位秒,-1 表示永远等待,0 表示不等待,立即抛出异常
|
|
47
|
+
wait_package_interval: float = 0.1 # 单位秒
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NemuIpcImpl(Touchable, Screenshotable):
|
|
51
|
+
"""
|
|
52
|
+
利用 MuMu12 提供的 external_renderer_ipc.dll 进行截图与触摸控制。
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: NemuIpcImplConfig):
|
|
56
|
+
self.config = config
|
|
57
|
+
self.__width: int = 0
|
|
58
|
+
self.__height: int = 0
|
|
59
|
+
self.__connected: bool = False
|
|
60
|
+
self._connect_id: int = 0
|
|
61
|
+
self.nemu_folder = config.nemu_folder
|
|
62
|
+
|
|
63
|
+
# --------------------------- DLL 封装 ---------------------------
|
|
64
|
+
self._ipc = ExternalRendererIpc(config.nemu_folder)
|
|
65
|
+
logger.info("ExternalRendererIpc initialized and DLL loaded")
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def width(self) -> int:
|
|
69
|
+
"""
|
|
70
|
+
屏幕宽度。
|
|
71
|
+
|
|
72
|
+
若为 0,表示未连接或未获取到分辨率。
|
|
73
|
+
"""
|
|
74
|
+
return self.__width
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def height(self) -> int:
|
|
78
|
+
"""
|
|
79
|
+
屏幕高度。
|
|
80
|
+
|
|
81
|
+
若为 0,表示未连接或未获取到分辨率。
|
|
82
|
+
"""
|
|
83
|
+
return self.__height
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def connected(self) -> bool:
|
|
87
|
+
"""是否已连接。"""
|
|
88
|
+
return self.__connected
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
# 基础控制
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def _ensure_connected(self) -> None:
|
|
95
|
+
if not self.__connected:
|
|
96
|
+
self.connect()
|
|
97
|
+
|
|
98
|
+
def _get_display_id(self) -> int:
|
|
99
|
+
"""获取有效的 display_id。"""
|
|
100
|
+
# 如果配置中直接指定了 display_id,直接返回
|
|
101
|
+
if self.config.display_id is not None:
|
|
102
|
+
return self.config.display_id
|
|
103
|
+
|
|
104
|
+
# 如果设置了 target_package_name,实时获取 display_id
|
|
105
|
+
if self.config.target_package_name:
|
|
106
|
+
self._ensure_connected()
|
|
107
|
+
|
|
108
|
+
timeout = self.config.wait_package_timeout
|
|
109
|
+
interval = self.config.wait_package_interval
|
|
110
|
+
if timeout == -1:
|
|
111
|
+
timeout = float('inf')
|
|
112
|
+
start_time = time.time()
|
|
113
|
+
while True:
|
|
114
|
+
display_id = self._ipc.get_display_id(
|
|
115
|
+
self._connect_id,
|
|
116
|
+
self.config.target_package_name,
|
|
117
|
+
self.config.app_index
|
|
118
|
+
)
|
|
119
|
+
if display_id >= 0:
|
|
120
|
+
return display_id
|
|
121
|
+
elif display_id == -1:
|
|
122
|
+
# 可以继续等
|
|
123
|
+
pass
|
|
124
|
+
else:
|
|
125
|
+
# 未知错误
|
|
126
|
+
raise NemuIpcError(f"Failed to get display_id for package '{self.config.target_package_name}', error code={display_id}")
|
|
127
|
+
if time.time() - start_time > timeout:
|
|
128
|
+
break
|
|
129
|
+
sleep(interval)
|
|
130
|
+
|
|
131
|
+
raise NemuIpcError(f"Failed to get display_id for package '{self.config.target_package_name}' within {timeout}s")
|
|
132
|
+
|
|
133
|
+
# 如果都没有设置,抛出错误
|
|
134
|
+
raise NemuIpcError("display_id is None and target_package_name is not set. Please set display_id or target_package_name in config.")
|
|
135
|
+
|
|
136
|
+
def connect(self) -> None:
|
|
137
|
+
"""连接模拟器。"""
|
|
138
|
+
if self.__connected:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
connect_id = self._ipc.connect(self.nemu_folder, self.config.instance_id)
|
|
142
|
+
if connect_id == 0:
|
|
143
|
+
raise NemuIpcError("nemu_connect failed, please check if the emulator is running and the instance ID is correct.")
|
|
144
|
+
|
|
145
|
+
self._connect_id = connect_id
|
|
146
|
+
self.__connected = True
|
|
147
|
+
logger.debug("NemuIpc connected, connect_id=%d", connect_id)
|
|
148
|
+
|
|
149
|
+
def disconnect(self) -> None:
|
|
150
|
+
"""断开连接。"""
|
|
151
|
+
if not self.__connected:
|
|
152
|
+
return
|
|
153
|
+
self._ipc.disconnect(self._connect_id)
|
|
154
|
+
self.__connected = False
|
|
155
|
+
self._connect_id = 0
|
|
156
|
+
logger.debug("NemuIpc disconnected.")
|
|
157
|
+
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
# Screenshotable 接口实现
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
@property
|
|
162
|
+
def screen_size(self) -> tuple[int, int]:
|
|
163
|
+
"""获取屏幕分辨率。"""
|
|
164
|
+
if self.__width == 0 or self.__height == 0:
|
|
165
|
+
self._refresh_resolution()
|
|
166
|
+
if self.__width == 0 or self.__height == 0:
|
|
167
|
+
raise NemuIpcError("Screen resolution not obtained, please connect to the emulator first.")
|
|
168
|
+
return self.__width, self.__height
|
|
169
|
+
|
|
170
|
+
@override
|
|
171
|
+
def detect_orientation(self):
|
|
172
|
+
return self.get_display_orientation(self._get_display_id())
|
|
173
|
+
|
|
174
|
+
def get_display_orientation(self, display_id: int = 0) -> Literal['portrait', 'landscape'] | None:
|
|
175
|
+
"""获取指定显示屏的方向。"""
|
|
176
|
+
width, height = self.query_resolution(display_id)
|
|
177
|
+
if width > height:
|
|
178
|
+
return "landscape"
|
|
179
|
+
if height > width:
|
|
180
|
+
return "portrait"
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
@override
|
|
184
|
+
def screenshot(self) -> MatLike:
|
|
185
|
+
self._ensure_connected()
|
|
186
|
+
|
|
187
|
+
# 必须每次都更新分辨率,因为屏幕可能会旋转
|
|
188
|
+
self._refresh_resolution()
|
|
189
|
+
|
|
190
|
+
length = self.__width * self.__height * 4 # RGBA
|
|
191
|
+
buf_type = ctypes.c_ubyte * length
|
|
192
|
+
buffer = buf_type()
|
|
193
|
+
|
|
194
|
+
w_ptr = ctypes.pointer(ctypes.c_int(self.__width))
|
|
195
|
+
h_ptr = ctypes.pointer(ctypes.c_int(self.__height))
|
|
196
|
+
|
|
197
|
+
ret = self._ipc.capture_display(
|
|
198
|
+
self._connect_id,
|
|
199
|
+
self._get_display_id(),
|
|
200
|
+
length,
|
|
201
|
+
ctypes.cast(w_ptr, ctypes.c_void_p),
|
|
202
|
+
ctypes.cast(h_ptr, ctypes.c_void_p),
|
|
203
|
+
ctypes.cast(buffer, ctypes.c_void_p),
|
|
204
|
+
)
|
|
205
|
+
if ret != 0:
|
|
206
|
+
raise NemuIpcError(f"nemu_capture_display screenshot failed, error code={ret}")
|
|
207
|
+
|
|
208
|
+
# 读入并转换数据
|
|
209
|
+
img = np.ctypeslib.as_array(buffer).reshape((self.__height, self.__width, 4))
|
|
210
|
+
# RGBA -> BGR
|
|
211
|
+
img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGR)
|
|
212
|
+
cv2.flip(img, 0, dst=img)
|
|
213
|
+
return img
|
|
214
|
+
|
|
215
|
+
# --------------------------- 内部工具 -----------------------------
|
|
216
|
+
|
|
217
|
+
def _refresh_resolution(self) -> None:
|
|
218
|
+
"""刷新分辨率信息。"""
|
|
219
|
+
display_id = self._get_display_id()
|
|
220
|
+
self.__width, self.__height = self.query_resolution(display_id)
|
|
221
|
+
|
|
222
|
+
def query_resolution(self, display_id: int = 0) -> tuple[int, int]:
|
|
223
|
+
"""
|
|
224
|
+
查询指定显示屏的分辨率。
|
|
225
|
+
|
|
226
|
+
:param display_id: 显示屏 ID。
|
|
227
|
+
:return: 分辨率 (width, height)。
|
|
228
|
+
:raise NemuIpcError: 查询失败。
|
|
229
|
+
"""
|
|
230
|
+
self._ensure_connected()
|
|
231
|
+
|
|
232
|
+
w_ptr = ctypes.pointer(ctypes.c_int(0))
|
|
233
|
+
h_ptr = ctypes.pointer(ctypes.c_int(0))
|
|
234
|
+
ret = self._ipc.capture_display(
|
|
235
|
+
self._connect_id,
|
|
236
|
+
display_id,
|
|
237
|
+
0,
|
|
238
|
+
ctypes.cast(w_ptr, ctypes.c_void_p),
|
|
239
|
+
ctypes.cast(h_ptr, ctypes.c_void_p),
|
|
240
|
+
ctypes.c_void_p(),
|
|
241
|
+
)
|
|
242
|
+
if ret != 0:
|
|
243
|
+
raise NemuIpcError(f"Call nemu_capture_display failed. Return value={ret}")
|
|
244
|
+
|
|
245
|
+
return w_ptr.contents.value, h_ptr.contents.value
|
|
246
|
+
|
|
247
|
+
# ------------------------------------------------------------------
|
|
248
|
+
# Touchable 接口实现
|
|
249
|
+
# ------------------------------------------------------------------
|
|
250
|
+
def __convert_pos(self, x: int, y: int) -> tuple[int, int]:
|
|
251
|
+
# Android 显示屏有两套坐标:逻辑坐标与物理坐标。
|
|
252
|
+
# 逻辑坐标原点始终是画面左上角,而物理坐标原点则始终是显示屏的左上角。
|
|
253
|
+
# 如果屏幕画面旋转,会导致两个坐标的原点不同,坐标也不同。
|
|
254
|
+
# ========
|
|
255
|
+
# 这里传给 MuMu 的是逻辑坐标,ExternalRendererIpc DLL 内部会
|
|
256
|
+
# 自动判断旋转,并转换为物理坐标。但是这部分有个 bug:
|
|
257
|
+
# 旋转没有考虑到多显示器,只是以主显示器为准,若两个显示器旋转不一致,
|
|
258
|
+
# 会导致错误地转换坐标。因此需要在 Python 层面 workaround 这个问题。
|
|
259
|
+
# 通过判断主显示器与当前显示器的旋转,将坐标进行预转换,抵消 DLL 层的错误转换。
|
|
260
|
+
display_id = self._get_display_id()
|
|
261
|
+
if display_id == 0:
|
|
262
|
+
return x, y
|
|
263
|
+
else:
|
|
264
|
+
primary = self.get_display_orientation(0)
|
|
265
|
+
primary_size = self.query_resolution(0)
|
|
266
|
+
current = self.get_display_orientation(display_id)
|
|
267
|
+
if primary == current:
|
|
268
|
+
return x, y
|
|
269
|
+
else:
|
|
270
|
+
# 如果旋转不一致,视为顺时针旋转了 90°
|
|
271
|
+
# 因此我们要提前逆时针旋转 90°
|
|
272
|
+
self._refresh_resolution()
|
|
273
|
+
x, y = y, primary_size[1] - x
|
|
274
|
+
return x, y
|
|
275
|
+
|
|
276
|
+
@override
|
|
277
|
+
def click(self, x: int, y: int) -> None:
|
|
278
|
+
self._ensure_connected()
|
|
279
|
+
display_id = self._get_display_id()
|
|
280
|
+
x, y = self.__convert_pos(x, y)
|
|
281
|
+
self._ipc.input_touch_down(self._connect_id, display_id, x, y)
|
|
282
|
+
sleep(0.01)
|
|
283
|
+
self._ipc.input_touch_up(self._connect_id, display_id)
|
|
284
|
+
|
|
285
|
+
@override
|
|
286
|
+
def swipe(
|
|
287
|
+
self,
|
|
288
|
+
x1: int,
|
|
289
|
+
y1: int,
|
|
290
|
+
x2: int,
|
|
291
|
+
y2: int,
|
|
292
|
+
duration: float | None = None,
|
|
293
|
+
) -> None:
|
|
294
|
+
self._ensure_connected()
|
|
295
|
+
|
|
296
|
+
duration = duration or 0.3
|
|
297
|
+
steps = max(int(duration / 0.01), 2)
|
|
298
|
+
display_id = self._get_display_id()
|
|
299
|
+
x1, y1 = self.__convert_pos(x1, y1)
|
|
300
|
+
x2, y2 = self.__convert_pos(x2, y2)
|
|
301
|
+
|
|
302
|
+
xs = np.linspace(x1, x2, steps, dtype=int)
|
|
303
|
+
ys = np.linspace(y1, y2, steps, dtype=int)
|
|
304
|
+
|
|
305
|
+
# 按下第一点
|
|
306
|
+
self._ipc.input_touch_down(self._connect_id, display_id, xs[0], ys[0])
|
|
307
|
+
sleep(0.01)
|
|
308
|
+
# 中间移动
|
|
309
|
+
for px, py in zip(xs[1:-1], ys[1:-1]):
|
|
310
|
+
self._ipc.input_touch_down(self._connect_id, display_id, px, py)
|
|
311
|
+
sleep(0.01)
|
|
312
|
+
|
|
313
|
+
# 最终抬起
|
|
314
|
+
self._ipc.input_touch_up(self._connect_id, display_id)
|
|
315
|
+
sleep(0.01)
|
|
316
|
+
|
|
317
|
+
if __name__ == '__main__':
|
|
318
|
+
nemu = NemuIpcImpl(NemuIpcImplConfig(
|
|
319
|
+
r'F:\Apps\Netease\MuMuPlayer-12.0', 0, None,
|
|
320
|
+
target_package_name='com.android.chrome',
|
|
321
|
+
))
|
|
322
|
+
nemu.connect()
|
|
323
|
+
# while True:
|
|
324
|
+
# nemu.click(0, 0)
|
|
325
|
+
nemu.click(100, 100)
|
|
326
|
+
nemu.click(100*3, 100)
|
|
327
|
+
nemu.click(100*3, 100*3)
|
|
@@ -14,6 +14,7 @@ import xmlrpc.server
|
|
|
14
14
|
from typing import Literal, cast, Any, Tuple
|
|
15
15
|
from functools import cached_property
|
|
16
16
|
from threading import Thread
|
|
17
|
+
from dataclasses import dataclass
|
|
17
18
|
|
|
18
19
|
import cv2
|
|
19
20
|
import numpy as np
|
|
@@ -22,10 +23,18 @@ from cv2.typing import MatLike
|
|
|
22
23
|
from kotonebot import logging
|
|
23
24
|
from ..device import Device, WindowsDevice
|
|
24
25
|
from ..protocol import Touchable, Screenshotable
|
|
25
|
-
from
|
|
26
|
+
from ..registration import ImplConfig
|
|
27
|
+
from .windows import WindowsImpl, WindowsImplConfig
|
|
26
28
|
|
|
27
29
|
logger = logging.getLogger(__name__)
|
|
28
30
|
|
|
31
|
+
# 定义配置模型
|
|
32
|
+
@dataclass
|
|
33
|
+
class RemoteWindowsImplConfig(ImplConfig):
|
|
34
|
+
windows_impl_config: WindowsImplConfig
|
|
35
|
+
host: str = "localhost"
|
|
36
|
+
port: int = 8000
|
|
37
|
+
|
|
29
38
|
def _encode_image(image: MatLike) -> str:
|
|
30
39
|
"""Encode an image as a base64 string."""
|
|
31
40
|
success, buffer = cv2.imencode('.png', image)
|
|
@@ -48,13 +57,17 @@ class RemoteWindowsServer:
|
|
|
48
57
|
This class wraps a WindowsImpl instance and exposes its methods via XML-RPC.
|
|
49
58
|
"""
|
|
50
59
|
|
|
51
|
-
def __init__(self, host="localhost", port=8000):
|
|
60
|
+
def __init__(self, windows_impl_config: WindowsImplConfig, host="localhost", port=8000):
|
|
52
61
|
"""Initialize the server with the given host and port."""
|
|
53
62
|
self.host = host
|
|
54
63
|
self.port = port
|
|
55
64
|
self.server = None
|
|
56
65
|
self.device = WindowsDevice()
|
|
57
|
-
self.impl = WindowsImpl(
|
|
66
|
+
self.impl = WindowsImpl(
|
|
67
|
+
WindowsDevice(),
|
|
68
|
+
ahk_exe_path=windows_impl_config.ahk_exe_path,
|
|
69
|
+
window_title=windows_impl_config.window_title
|
|
70
|
+
)
|
|
58
71
|
self.device._screenshot = self.impl
|
|
59
72
|
self.device._touch = self.impl
|
|
60
73
|
|
|
@@ -177,21 +190,4 @@ class RemoteWindowsImpl(Touchable, Screenshotable):
|
|
|
177
190
|
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> None:
|
|
178
191
|
"""Swipe from (x1, y1) to (x2, y2) on the remote server."""
|
|
179
192
|
if not self.proxy.swipe(x1, y1, x2, y2, duration):
|
|
180
|
-
raise RuntimeError(f"Failed to swipe from ({x1}, {y1}) to ({x2}, {y2})")
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if __name__ == "__main__":
|
|
184
|
-
import argparse
|
|
185
|
-
|
|
186
|
-
parser = argparse.ArgumentParser(description="Remote Windows XML-RPC Server")
|
|
187
|
-
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
|
|
188
|
-
parser.add_argument("--port", type=int, default=8000, help="Port to bind to")
|
|
189
|
-
args = parser.parse_args()
|
|
190
|
-
|
|
191
|
-
logging.basicConfig(level=logging.INFO)
|
|
192
|
-
|
|
193
|
-
server = RemoteWindowsServer(args.host, args.port)
|
|
194
|
-
try:
|
|
195
|
-
server.start()
|
|
196
|
-
except KeyboardInterrupt:
|
|
197
|
-
logger.info("Server stopped by user")
|
|
193
|
+
raise RuntimeError(f"Failed to swipe from ({x1}, {y1}) to ({x2}, {y2})")
|
|
@@ -4,6 +4,7 @@ from typing import Literal
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import uiautomator2 as u2
|
|
6
6
|
from cv2.typing import MatLike
|
|
7
|
+
from adbutils._device import AdbDevice as AdbUtilsDevice
|
|
7
8
|
|
|
8
9
|
from kotonebot import logging
|
|
9
10
|
from ..device import Device
|
|
@@ -14,9 +15,8 @@ logger = logging.getLogger(__name__)
|
|
|
14
15
|
SCREENSHOT_INTERVAL = 0.2
|
|
15
16
|
|
|
16
17
|
class UiAutomator2Impl(Screenshotable, Commandable, Touchable):
|
|
17
|
-
def __init__(self,
|
|
18
|
-
self.
|
|
19
|
-
self.u2_client = u2.Device(device.adb.serial)
|
|
18
|
+
def __init__(self, adb_connection: AdbUtilsDevice):
|
|
19
|
+
self.u2_client = u2.Device(adb_connection.serial)
|
|
20
20
|
self.__last_screenshot_time = 0
|
|
21
21
|
|
|
22
22
|
def screenshot(self) -> MatLike:
|
|
@@ -38,10 +38,7 @@ class UiAutomator2Impl(Screenshotable, Commandable, Touchable):
|
|
|
38
38
|
def screen_size(self) -> tuple[int, int]:
|
|
39
39
|
info = self.u2_client.info
|
|
40
40
|
sizes = info['displayWidth'], info['displayHeight']
|
|
41
|
-
|
|
42
|
-
return (max(sizes), min(sizes))
|
|
43
|
-
else:
|
|
44
|
-
return (min(sizes), max(sizes))
|
|
41
|
+
return sizes
|
|
45
42
|
|
|
46
43
|
def detect_orientation(self) -> Literal['portrait', 'landscape'] | None:
|
|
47
44
|
"""
|
|
@@ -82,4 +79,4 @@ class UiAutomator2Impl(Screenshotable, Commandable, Touchable):
|
|
|
82
79
|
"""
|
|
83
80
|
滑动屏幕
|
|
84
81
|
"""
|
|
85
|
-
self.u2_client.swipe(x1, y1, x2, y2, duration=duration or 0.1)
|
|
82
|
+
self.u2_client.swipe(x1, y1, x2, y2, duration=duration or 0.1)
|
|
@@ -2,6 +2,7 @@ from ctypes import windll
|
|
|
2
2
|
from typing import Literal
|
|
3
3
|
from importlib import resources
|
|
4
4
|
from functools import cached_property
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
|
|
6
7
|
import cv2
|
|
7
8
|
import win32ui
|
|
@@ -10,14 +11,21 @@ import numpy as np
|
|
|
10
11
|
from ahk import AHK, MsgBoxIcon
|
|
11
12
|
from cv2.typing import MatLike
|
|
12
13
|
|
|
13
|
-
from ..device import Device
|
|
14
|
+
from ..device import Device, WindowsDevice
|
|
14
15
|
from ..protocol import Commandable, Touchable, Screenshotable
|
|
16
|
+
from ..registration import ImplConfig
|
|
17
|
+
|
|
18
|
+
# 1. 定义配置模型
|
|
19
|
+
@dataclass
|
|
20
|
+
class WindowsImplConfig(ImplConfig):
|
|
21
|
+
window_title: str
|
|
22
|
+
ahk_exe_path: str
|
|
15
23
|
|
|
16
24
|
class WindowsImpl(Touchable, Screenshotable):
|
|
17
|
-
def __init__(self, device: Device):
|
|
25
|
+
def __init__(self, device: Device, window_title: str, ahk_exe_path: str):
|
|
18
26
|
self.__hwnd: int | None = None
|
|
19
|
-
|
|
20
|
-
self.ahk = AHK(executable_path=
|
|
27
|
+
self.window_title = window_title
|
|
28
|
+
self.ahk = AHK(executable_path=ahk_exe_path)
|
|
21
29
|
self.device = device
|
|
22
30
|
|
|
23
31
|
# 设置 DPI aware,否则高缩放显示器上返回的坐标会错误
|
|
@@ -43,21 +51,12 @@ class WindowsImpl(Touchable, Screenshotable):
|
|
|
43
51
|
# 将点击坐标设置为相对 Client
|
|
44
52
|
self.ahk.set_coord_mode('Mouse', 'Client')
|
|
45
53
|
|
|
46
|
-
@cached_property
|
|
47
|
-
def scale_ratio(self) -> float:
|
|
48
|
-
"""
|
|
49
|
-
缩放比例。截图与模拟输入前都会根据这个比例缩放。
|
|
50
|
-
"""
|
|
51
|
-
left, _, right, _ = self.__client_rect()
|
|
52
|
-
w = right - left
|
|
53
|
-
return 720 / w
|
|
54
|
-
|
|
55
54
|
@property
|
|
56
55
|
def hwnd(self) -> int:
|
|
57
56
|
if self.__hwnd is None:
|
|
58
|
-
self.__hwnd = win32gui.FindWindow(None,
|
|
57
|
+
self.__hwnd = win32gui.FindWindow(None, self.window_title)
|
|
59
58
|
if self.__hwnd is None or self.__hwnd == 0:
|
|
60
|
-
raise RuntimeError('Failed to find window')
|
|
59
|
+
raise RuntimeError(f'Failed to find window: {self.window_title}')
|
|
61
60
|
return self.__hwnd
|
|
62
61
|
|
|
63
62
|
def __client_rect(self) -> tuple[int, int, int, int]:
|
|
@@ -73,8 +72,8 @@ class WindowsImpl(Touchable, Screenshotable):
|
|
|
73
72
|
return win32gui.ClientToScreen(hwnd, (x, y))
|
|
74
73
|
|
|
75
74
|
def screenshot(self) -> MatLike:
|
|
76
|
-
if not self.ahk.win_is_active(
|
|
77
|
-
self.ahk.win_activate(
|
|
75
|
+
if not self.ahk.win_is_active(self.window_title):
|
|
76
|
+
self.ahk.win_activate(self.window_title)
|
|
78
77
|
hwnd = self.hwnd
|
|
79
78
|
|
|
80
79
|
# TODO: 需要检查下面这些 WinAPI 的返回结果
|
|
@@ -116,21 +115,17 @@ class WindowsImpl(Touchable, Screenshotable):
|
|
|
116
115
|
|
|
117
116
|
# 将 RGBA 转换为 RGB
|
|
118
117
|
cropped_im = cv2.cvtColor(cropped_im, cv2.COLOR_RGBA2RGB)
|
|
119
|
-
# 缩放
|
|
120
|
-
cropped_im = cv2.resize(cropped_im, None, fx=self.scale_ratio, fy=self.scale_ratio)
|
|
121
118
|
return cropped_im
|
|
122
119
|
|
|
123
120
|
@property
|
|
124
121
|
def screen_size(self) -> tuple[int, int]:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
else:
|
|
130
|
-
return 720, 1280
|
|
122
|
+
left, top, right, bot = self.__client_rect()
|
|
123
|
+
w = right - left
|
|
124
|
+
h = bot - top
|
|
125
|
+
return w, h
|
|
131
126
|
|
|
132
127
|
def detect_orientation(self) -> None | Literal['portrait'] | Literal['landscape']:
|
|
133
|
-
pos = self.ahk.win_get_position(
|
|
128
|
+
pos = self.ahk.win_get_position(self.window_title)
|
|
134
129
|
if pos is None:
|
|
135
130
|
return None
|
|
136
131
|
w, h = pos.width, pos.height
|
|
@@ -146,25 +141,22 @@ class WindowsImpl(Touchable, Screenshotable):
|
|
|
146
141
|
x = 2
|
|
147
142
|
if y == 0:
|
|
148
143
|
y = 2
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
self.ahk.win_activate('gakumas')
|
|
144
|
+
if not self.ahk.win_is_active(self.window_title):
|
|
145
|
+
self.ahk.win_activate(self.window_title)
|
|
152
146
|
self.ahk.click(x, y)
|
|
153
147
|
|
|
154
148
|
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> None:
|
|
155
|
-
if not self.ahk.win_is_active(
|
|
156
|
-
self.ahk.win_activate(
|
|
157
|
-
x1, y1 = int(x1 / self.scale_ratio), int(y1 / self.scale_ratio)
|
|
158
|
-
x2, y2 = int(x2 / self.scale_ratio), int(y2 / self.scale_ratio)
|
|
149
|
+
if not self.ahk.win_is_active(self.window_title):
|
|
150
|
+
self.ahk.win_activate(self.window_title)
|
|
159
151
|
# TODO: 这个 speed 的单位是什么?
|
|
160
152
|
self.ahk.mouse_drag(x2, y2, from_position=(x1, y1), coord_mode='Client', speed=10)
|
|
161
153
|
|
|
162
|
-
|
|
163
154
|
if __name__ == '__main__':
|
|
164
155
|
from ..device import Device
|
|
165
|
-
from time import sleep
|
|
166
156
|
device = Device()
|
|
167
|
-
|
|
157
|
+
# 在测试环境中直接使用默认路径
|
|
158
|
+
ahk_path = str(resources.files('kaa.res.bin') / 'AutoHotkey.exe')
|
|
159
|
+
impl = WindowsImpl(device, window_title='gakumas', ahk_exe_path=ahk_path)
|
|
168
160
|
device._screenshot = impl
|
|
169
161
|
device._touch = impl
|
|
170
162
|
device.swipe_scaled(0.5, 0.8, 0.5, 0.2)
|
kotonebot/client/protocol.py
CHANGED
|
@@ -28,6 +28,19 @@ class Commandable(Protocol):
|
|
|
28
28
|
def launch_app(self, package_name: str) -> None: ...
|
|
29
29
|
def current_package(self) -> str | None: ...
|
|
30
30
|
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class AndroidCommandable(Protocol):
|
|
33
|
+
"""定义 Android 平台的特定命令"""
|
|
34
|
+
def launch_app(self, package_name: str) -> None: ...
|
|
35
|
+
def current_package(self) -> str | None: ...
|
|
36
|
+
def adb_shell(self, cmd: str) -> str: ...
|
|
37
|
+
|
|
38
|
+
@runtime_checkable
|
|
39
|
+
class WindowsCommandable(Protocol):
|
|
40
|
+
"""定义 Windows 平台的特定命令"""
|
|
41
|
+
def get_foreground_window(self) -> tuple[int, str]: ...
|
|
42
|
+
def exec_command(self, command: str) -> tuple[int, str, str]: ...
|
|
43
|
+
|
|
31
44
|
@runtime_checkable
|
|
32
45
|
class Screenshotable(Protocol):
|
|
33
46
|
def __init__(self, device: 'Device'): ...
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TypeVar, Callable, Dict, Type, Any, overload, Literal, cast, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from ..errors import KotonebotError
|
|
5
|
+
from .device import Device
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .implements.adb import AdbImplConfig
|
|
8
|
+
from .implements.remote_windows import RemoteWindowsImplConfig
|
|
9
|
+
from .implements.windows import WindowsImplConfig
|
|
10
|
+
from .implements.nemu_ipc import NemuIpcImplConfig
|
|
11
|
+
|
|
12
|
+
AdbBasedImpl = Literal['adb', 'adb_raw', 'uiautomator2']
|
|
13
|
+
DeviceImpl = str | AdbBasedImpl | Literal['windows', 'remote_windows', 'nemu_ipc']
|
|
14
|
+
|
|
15
|
+
# --- 核心类型定义 ---
|
|
16
|
+
|
|
17
|
+
class ImplRegistrationError(KotonebotError):
|
|
18
|
+
"""与 impl 注册相关的错误"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ImplConfig:
|
|
23
|
+
"""所有设备实现配置模型的名义上的基类,便于类型约束。"""
|
|
24
|
+
pass
|
kotonebot/config/base_config.py
CHANGED
|
@@ -3,10 +3,10 @@ from typing import Generic, TypeVar, Literal
|
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel, ConfigDict
|
|
5
5
|
|
|
6
|
-
from kotonebot.client.factory import DeviceImpl
|
|
7
6
|
|
|
8
7
|
T = TypeVar('T')
|
|
9
8
|
BackendType = Literal['custom', 'mumu12', 'leidian', 'dmm']
|
|
9
|
+
DeviceRecipes = Literal['adb', 'adb_raw', 'uiautomator2', 'windows', 'remote_windows', 'nemu_ipc']
|
|
10
10
|
|
|
11
11
|
class ConfigBaseModel(BaseModel):
|
|
12
12
|
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
@@ -27,7 +27,7 @@ class BackendConfig(ConfigBaseModel):
|
|
|
27
27
|
雷电模拟器需要设置正确的模拟器名,否则 自动启动模拟器 功能将无法正常工作。
|
|
28
28
|
其他功能不受影响。
|
|
29
29
|
"""
|
|
30
|
-
screenshot_impl:
|
|
30
|
+
screenshot_impl: DeviceRecipes = 'adb'
|
|
31
31
|
"""
|
|
32
32
|
截图方法。暂时推荐使用【adb】截图方式。
|
|
33
33
|
|
|
@@ -44,6 +44,12 @@ class BackendConfig(ConfigBaseModel):
|
|
|
44
44
|
"""模拟器 exe 文件路径"""
|
|
45
45
|
emulator_args: str = ""
|
|
46
46
|
"""模拟器启动时的命令行参数"""
|
|
47
|
+
windows_window_title: str = 'gakumas'
|
|
48
|
+
"""Windows 截图方式的窗口标题"""
|
|
49
|
+
windows_ahk_path: str | None = None
|
|
50
|
+
"""Windows 截图方式的 AutoHotkey 可执行文件路径,为 None 时使用默认路径"""
|
|
51
|
+
mumu_background_mode: bool = False
|
|
52
|
+
"""MuMu12 模拟器后台保活模式"""
|
|
47
53
|
|
|
48
54
|
class PushConfig(ConfigBaseModel):
|
|
49
55
|
"""推送配置。"""
|
kotonebot/debug_entry.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
sys.path.append('./projects')
|
|
1
3
|
import runpy
|
|
2
4
|
import logging
|
|
3
5
|
import argparse
|
|
@@ -12,15 +14,20 @@ def run_script(script_path: str) -> None:
|
|
|
12
14
|
Args:
|
|
13
15
|
script_path: Python 脚本的路径
|
|
14
16
|
"""
|
|
17
|
+
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
|
15
18
|
# 获取模块名
|
|
16
|
-
module_name = script_path.strip('.py').replace('\\', '/').strip('/').replace('/', '.')
|
|
19
|
+
module_name = script_path.strip('.py').lstrip('projects/').replace('\\', '/').strip('/').replace('/', '.')
|
|
17
20
|
|
|
18
21
|
print(f"正在运行脚本: {script_path}")
|
|
19
22
|
# 运行脚本
|
|
20
|
-
from kotonebot.backend.context import init_context
|
|
21
|
-
|
|
23
|
+
from kotonebot.backend.context import init_context, manual_context
|
|
24
|
+
from kotonebot.kaa.main.kaa import Kaa
|
|
22
25
|
logging.getLogger('kotonebot').setLevel(logging.DEBUG)
|
|
23
|
-
|
|
26
|
+
config_path = './config.json'
|
|
27
|
+
kaa_instance = Kaa(config_path)
|
|
28
|
+
init_context(config_type=BaseConfig, target_device=kaa_instance._on_create_device())
|
|
29
|
+
kaa_instance._on_after_init_context()
|
|
30
|
+
manual_context().begin()
|
|
24
31
|
runpy.run_module(module_name, run_name="__main__")
|
|
25
32
|
|
|
26
33
|
def main():
|
kotonebot/errors.py
CHANGED
|
@@ -24,3 +24,10 @@ class TaskNotFoundError(KotonebotError):
|
|
|
24
24
|
def __init__(self, task_id: str):
|
|
25
25
|
self.task_id = task_id
|
|
26
26
|
super().__init__(f'Task "{task_id}" not found.')
|
|
27
|
+
|
|
28
|
+
class UnscalableResolutionError(KotonebotError):
|
|
29
|
+
def __init__(self, target_resolution: tuple[int, int], screen_size: tuple[int, int]):
|
|
30
|
+
self.target_resolution = target_resolution
|
|
31
|
+
self.screen_size = screen_size
|
|
32
|
+
super().__init__(f'Cannot scale to target resolution {target_resolution}. '
|
|
33
|
+
f'Screen size: {screen_size}')
|