ksaa 2025.5.16.1__py3-none-any.whl → 2025.5.23.1__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/__init__.py +0 -2
- kotonebot/backend/bot.py +19 -34
- kotonebot/backend/color.py +20 -17
- kotonebot/backend/context/context.py +41 -8
- kotonebot/backend/core.py +7 -30
- kotonebot/backend/dispatch.py +1 -1
- kotonebot/backend/image.py +26 -26
- kotonebot/backend/ocr.py +18 -17
- kotonebot/backend/preprocessor.py +0 -1
- kotonebot/client/__init__.py +8 -1
- kotonebot/client/device.py +7 -20
- kotonebot/client/factory.py +33 -5
- kotonebot/client/host/__init__.py +8 -1
- kotonebot/client/host/custom.py +7 -2
- kotonebot/client/host/leidian_host.py +200 -0
- kotonebot/client/host/mumu12_host.py +177 -0
- kotonebot/client/host/protocol.py +68 -38
- kotonebot/client/implements/adb.py +1 -1
- kotonebot/client/protocol.py +1 -1
- kotonebot/config/base_config.py +8 -3
- kotonebot/interop/win/reg.py +37 -0
- kotonebot/kaa/common.py +18 -2
- kotonebot/kaa/game_ui/badge.py +6 -6
- kotonebot/kaa/game_ui/common.py +6 -6
- kotonebot/kaa/game_ui/commu_event_buttons.py +4 -3
- kotonebot/kaa/game_ui/idols_overview.py +6 -6
- kotonebot/kaa/game_ui/schedule.py +162 -0
- kotonebot/kaa/game_ui/scrollable.py +7 -6
- kotonebot/kaa/kaa_context.py +8 -0
- kotonebot/kaa/main/cli.py +7 -0
- kotonebot/kaa/main/dmm_host.py +53 -0
- kotonebot/kaa/main/gr.py +508 -298
- kotonebot/kaa/main/kaa.py +77 -1
- kotonebot/kaa/metadata.py +26 -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/sprites/4503db6b-7224-4b81-9971-e7cfa56e10f2.png +0 -0
- kotonebot/kaa/tasks/R.py +137 -133
- kotonebot/kaa/tasks/daily/contest.py +123 -89
- kotonebot/kaa/tasks/daily/mission_reward.py +4 -4
- kotonebot/kaa/tasks/end_game.py +2 -9
- kotonebot/kaa/tasks/produce/cards.py +7 -6
- kotonebot/kaa/tasks/produce/common.py +16 -12
- kotonebot/kaa/tasks/produce/in_purodyuusu.py +5 -5
- kotonebot/kaa/tasks/produce/p_drink.py +4 -3
- kotonebot/kaa/tasks/produce/produce.py +5 -3
- kotonebot/kaa/tasks/start_game.py +2 -2
- kotonebot/primitives/__init__.py +17 -0
- kotonebot/primitives/geometry.py +290 -0
- kotonebot/primitives/visual.py +63 -0
- kotonebot/util.py +20 -20
- {ksaa-2025.5.16.1.dist-info → ksaa-2025.5.23.1.dist-info}/METADATA +1 -1
- {ksaa-2025.5.16.1.dist-info → ksaa-2025.5.23.1.dist-info}/RECORD +191 -183
- {ksaa-2025.5.16.1.dist-info → ksaa-2025.5.23.1.dist-info}/WHEEL +1 -1
- /kotonebot/{client/host/mumu_host.py → interop/win/__init__.py} +0 -0
- /kotonebot/kaa/sprites/{7971b595-44d3-4ce7-91ae-a8e5a25c543b.png → 02ff2b86-db5a-4850-ac98-4288a3f71ed8.png} +0 -0
- /kotonebot/kaa/sprites/{bc64dd37-48ba-45a5-af6d-457a7fb2c9fd.png → 04422a5d-2499-40fd-a08f-095ecbd051a8.png} +0 -0
- /kotonebot/kaa/sprites/{17da97eb-93c5-438f-8688-0be5b0756019.png → 0672e1c6-6abc-452c-82b3-af6eb7454cfa.png} +0 -0
- /kotonebot/kaa/sprites/{5089ea64-3f32-4520-a252-4ee278815cdd.png → 07960be4-b575-4f7f-be64-7e344dc118e0.png} +0 -0
- /kotonebot/kaa/sprites/{0f67c0a6-45d0-4953-b1a9-b5eb2c916173.png → 09cd4336-7d2a-4bce-9d08-8ac64a2bdcc3.png} +0 -0
- /kotonebot/kaa/sprites/{5ed9bef4-ab11-4186-9b21-a479eec687ce.png → 09fda5a8-c546-48f0-ae29-ebe4f4208d8b.png} +0 -0
- /kotonebot/kaa/sprites/{7dec3bd9-f8ee-430f-af54-0c03c4c91546.png → 0b5c2d3f-28b1-456f-a452-05ca006f8b33.png} +0 -0
- /kotonebot/kaa/sprites/{76b0e3b3-3438-4e88-aca8-da18c8e50ddd.png → 0c359f2a-72d4-4203-9ca3-968a1948eee8.png} +0 -0
- /kotonebot/kaa/sprites/{9e110778-1b66-4700-b462-64c1dea368da.png → 0ecc3e37-189c-40ad-9ee6-fb576b72a13b.png} +0 -0
- /kotonebot/kaa/sprites/{bbcb97e2-89b7-4de2-b480-a0442fc33b1e.png → 11c2ff97-3303-4d6d-8e4f-6da2148bbe20.png} +0 -0
- /kotonebot/kaa/sprites/{36d712ce-9227-4aef-86ee-4f78415b0c80.png → 128886a7-7629-4158-9538-3619542f775b.png} +0 -0
- /kotonebot/kaa/sprites/{ba07fa5e-3451-4520-8b10-0b240e5decec.png → 13a85dbd-e226-47cc-b6f5-c0d3b40654f5.png} +0 -0
- /kotonebot/kaa/sprites/{f71df091-fc71-48d4-8124-b3c46f4c4436.png → 13c8fa4a-0ae5-4ee1-a433-20f6c724466e.png} +0 -0
- /kotonebot/kaa/sprites/{02e69560-ec42-4453-b771-8600b5726b93.png → 18042dda-0222-4333-9d79-3afc3dc75220.png} +0 -0
- /kotonebot/kaa/sprites/{420e4ff4-82a3-4ea3-98a7-63efe65003a6.png → 18670c18-07e3-4378-a8fc-4f9b331db00f.png} +0 -0
- /kotonebot/kaa/sprites/{027dd54f-09d8-452d-8eff-66b06aecd264.png → 1959c932-1bbf-4fee-b3a1-2ecfbc6d4209.png} +0 -0
- /kotonebot/kaa/sprites/{b673cc06-14ef-4a22-b484-13a17b01b0d4.png → 1a8ec83b-ebba-4ff7-9737-104d3aa2ff98.png} +0 -0
- /kotonebot/kaa/sprites/{1e6d734e-7bc5-4f3f-a717-3931f44a7709.png → 1b6bc9ae-3f4e-4722-ae6f-d76bd65b071e.png} +0 -0
- /kotonebot/kaa/sprites/{0bd326fd-fad0-4e6c-957c-da08583ef231.png → 1e4671aa-2cdc-4432-9d83-c635e0a3d09d.png} +0 -0
- /kotonebot/kaa/sprites/{e941ae91-59bf-4a69-945b-1bcd7cf241c6.png → 2490bd58-bcf7-4c94-ab24-04e752137939.png} +0 -0
- /kotonebot/kaa/sprites/{f790aed5-b9f3-4c93-a14f-47629f53052d.png → 25cfb896-fed1-4361-ae64-fc45fc8a98dd.png} +0 -0
- /kotonebot/kaa/sprites/{6e81e54e-b69b-434a-8bbf-6e2551bddeb8.png → 2730beb1-a6c5-463d-9748-393764bc8e34.png} +0 -0
- /kotonebot/kaa/sprites/{3f0b91e1-898a-4f44-b116-38483bc30461.png → 2766af5c-4ad1-44e2-ab84-e8a6ac7bb172.png} +0 -0
- /kotonebot/kaa/sprites/{4fca090a-b94a-433a-8438-832a54ff65e8.png → 2cc68858-7af0-4a12-8c78-c4e4dcf9fff1.png} +0 -0
- /kotonebot/kaa/sprites/{04b4ccde-a374-4df3-b0fa-552ae1518348.png → 2e68066d-9a56-48cc-8711-1d4c080ee320.png} +0 -0
- /kotonebot/kaa/sprites/{4a9e612b-9e52-4eb5-a851-3e5ad68030e9.png → 32d50687-b3a2-4b34-a2f9-4418b5e14fff.png} +0 -0
- /kotonebot/kaa/sprites/{2179ff5c-7964-42b1-9a95-c0bd8af1b692.png → 344bedf1-fcca-4025-9ff6-cea99c91ddb4.png} +0 -0
- /kotonebot/kaa/sprites/{99e95c59-d5da-448d-b839-51428b2686f0.png → 346da82f-bfca-4a0f-bae4-ca9c8e4b794e.png} +0 -0
- /kotonebot/kaa/sprites/{be7504d5-48e5-404c-a74d-f7fa86acf276.png → 379a8811-932c-4c75-854f-943631c921d5.png} +0 -0
- /kotonebot/kaa/sprites/{d11d8839-90ef-433c-8555-fad5478009a0.png → 38f8f55f-e9dc-4c10-b97a-b7254d871ae9.png} +0 -0
- /kotonebot/kaa/sprites/{03c9182c-b5ba-4404-b6f9-50b06a86e102.png → 39b1738f-af9a-4c85-9315-2858f0b1d1ab.png} +0 -0
- /kotonebot/kaa/sprites/{fb529219-fc53-43e1-83ca-2313d1d33636.png → 3a58e019-6328-412a-9d2f-85a73900371e.png} +0 -0
- /kotonebot/kaa/sprites/{d4673e38-cf76-4062-9a71-0f31936fe49b.png → 3c2f70b5-9095-48d6-a012-4d68a6b0a975.png} +0 -0
- /kotonebot/kaa/sprites/{e753b92e-99bf-4d81-9673-92b5f67d129b.png → 3d016208-f8ed-45be-bfb1-ed15b231d0b8.png} +0 -0
- /kotonebot/kaa/sprites/{0b0e76ce-60cc-45cc-b204-227e59d39c26.png → 3e2af23e-041f-4128-8a78-cbd1297538cf.png} +0 -0
- /kotonebot/kaa/sprites/{edb144e1-7c82-4fcf-b426-c6294ad2112d.png → 429c30b5-7d3a-40a1-853a-ec51c8f238e7.png} +0 -0
- /kotonebot/kaa/sprites/{de619141-10a4-4447-89b3-6cabcbf52d8a.png → 440eeea3-b710-485e-ab40-80e2b6ce66f0.png} +0 -0
- /kotonebot/kaa/sprites/{1607c23d-3ab4-418b-8114-1a3bb782d246.png → 46f24175-8657-497d-97e6-1c1ee356e4cd.png} +0 -0
- /kotonebot/kaa/sprites/{3eb037db-4cd8-4c00-a655-fa73f612adfd.png → 4a128979-bf11-42fe-a966-a8f5ee143128.png} +0 -0
- /kotonebot/kaa/sprites/{7b07a31c-9e2b-4ce0-8998-ccc0592de5e0.png → 4a5ec641-38ec-46b0-83c4-d7edacb754b0.png} +0 -0
- /kotonebot/kaa/sprites/{e97e58cb-3412-4a8c-9a9e-dd45daa5ae02.png → 4a9c1ac7-0a61-4d40-b749-e8896322581f.png} +0 -0
- /kotonebot/kaa/sprites/{9fd268df-96b7-4b48-a6d1-b5da2161da8e.png → 4af7631b-3674-4491-a3fb-a514bfd6b204.png} +0 -0
- /kotonebot/kaa/sprites/{32a1072b-c359-4dad-80f6-c9ca558e5f99.png → 4c2c5378-d946-41c2-980e-afb8493f71ee.png} +0 -0
- /kotonebot/kaa/sprites/{1a668ad4-d6b0-4aed-bb99-8b1ac85659d3.png → 4c4dd8e5-5370-4381-b6d4-c260b32015cf.png} +0 -0
- /kotonebot/kaa/sprites/{32793e3b-613b-4b6d-bc2e-25f9993f6274.png → 4c5b8c40-3dfa-4091-95c3-257a36e9a6d3.png} +0 -0
- /kotonebot/kaa/sprites/{d3fa5988-1184-4478-abb9-1c9799bd4100.png → 4d0b8533-b3e2-4ea1-8204-a8d5e67e1f64.png} +0 -0
- /kotonebot/kaa/sprites/{0de838fb-24e9-4fe8-8e2a-cc26bce6d7d8.png → 50e462d5-ca14-4934-b35a-da3ebb8fb33f.png} +0 -0
- /kotonebot/kaa/sprites/{d2830bf7-e0c4-4d00-868a-6df22bdf6fcd.png → 557389eb-d0da-4c5a-a322-de3e32a81909.png} +0 -0
- /kotonebot/kaa/sprites/{979c7468-d9ef-4cc1-9449-2aa716caabb0.png → 57b7006a-ada9-4d9d-9655-71765251882e.png} +0 -0
- /kotonebot/kaa/sprites/{8f39322e-92d2-4cc6-80ef-341884b0a598.png → 59ef821b-4803-4fe3-83b8-305902f8459a.png} +0 -0
- /kotonebot/kaa/sprites/{a0b83694-4945-4476-aa2e-f268841ac372.png → 5af71b93-310c-48ab-b438-dba296fb9e52.png} +0 -0
- /kotonebot/kaa/sprites/{a03e79b3-7f44-4211-b625-2510ba8031bc.png → 5b6b5bf8-cd03-4360-9550-e15d8db6241a.png} +0 -0
- /kotonebot/kaa/sprites/{2fd33c51-b3a7-4ffb-9555-d25961186eee.png → 5d963589-623d-4a49-9c72-af91134720b0.png} +0 -0
- /kotonebot/kaa/sprites/{f4c8a757-674e-45ca-af2c-0bc409b7bca8.png → 5f01b811-9aad-4439-a70b-c099a42c36fa.png} +0 -0
- /kotonebot/kaa/sprites/{a1c29e77-138c-4580-a777-666815039524.png → 601d0743-7143-4c04-989b-b80fc620e678.png} +0 -0
- /kotonebot/kaa/sprites/{4aed7e90-76b1-4837-a6ad-e2ed2e3240ec.png → 63e71ec7-6290-4efa-89cf-69c53d805a58.png} +0 -0
- /kotonebot/kaa/sprites/{c15ad104-3814-467c-a211-1e54be35fc66.png → 66f5fab8-274c-4ab9-983d-ca2f8d90ea94.png} +0 -0
- /kotonebot/kaa/sprites/{8031a52b-afe1-4ebd-9599-1d123bc0e48b.png → 6abb63c1-025a-425e-8f33-29f83f504d75.png} +0 -0
- /kotonebot/kaa/sprites/{ad26884a-8371-420f-be89-7fe82ddaf1e8.png → 6d6550a6-f270-474e-b55c-b2017ef87ab2.png} +0 -0
- /kotonebot/kaa/sprites/{bf158b17-4d93-40f1-b9cf-4ae9226bcfc1.png → 7057e165-9ed4-485e-9609-94854ef269dd.png} +0 -0
- /kotonebot/kaa/sprites/{fa64ed03-4277-4ae2-a958-adb93c393457.png → 78c2c39e-6621-41d1-9a1e-7e171b001795.png} +0 -0
- /kotonebot/kaa/sprites/{f32e04e8-2e9a-49c2-9734-d0a02ed2d892.png → 798a9587-3f8c-424e-90af-89a08ed62ced.png} +0 -0
- /kotonebot/kaa/sprites/{b677c2ba-62da-4afd-8ba7-a9b40362b5bb.png → 7bbac9cd-417f-4bd5-975a-a8a25c7bd922.png} +0 -0
- /kotonebot/kaa/sprites/{d1a8f72b-813b-4d8a-a596-5576758d7fe2.png → 7c2f9216-56f2-4eec-8a45-1bd864316dad.png} +0 -0
- /kotonebot/kaa/sprites/{fe7e4f2b-b795-43b6-bf0a-4e65c9133a40.png → 7c992bae-5ded-473b-8e60-4dede8434e5d.png} +0 -0
- /kotonebot/kaa/sprites/{e73cacc9-08aa-4b7a-acd3-3dbe95d9f51c.png → 7e7e8772-b31a-4bbe-919e-90373a4ad5a7.png} +0 -0
- /kotonebot/kaa/sprites/{39151ffc-365e-45b2-a2f4-9d4c76d41524.png → 80171520-d132-4395-b2ed-e6451335b3d5.png} +0 -0
- /kotonebot/kaa/sprites/{04180b4a-1532-42dd-9280-0f47c1e68959.png → 82cf688e-ac85-4425-917d-10b6db9b6cc3.png} +0 -0
- /kotonebot/kaa/sprites/{ebcaaba7-57f8-4bcb-93da-a288a0fcc95e.png → 87292951-43c1-4039-9b92-c59c1b25b5f7.png} +0 -0
- /kotonebot/kaa/sprites/{4dd7db2d-3107-4fc5-8f30-cfa70bc28de1.png → 893a6bba-905e-4123-9e35-f829fd6c8b2a.png} +0 -0
- /kotonebot/kaa/sprites/{f9e0be54-237b-4092-8784-1a4d2eb43097.png → 8a605d31-3160-49d1-9098-45d8ee18e8d6.png} +0 -0
- /kotonebot/kaa/sprites/{334d4915-4600-4c81-bbb0-c52291b7d089.png → 8cb3b6c2-7fae-41cf-8f33-a0e3ea668f25.png} +0 -0
- /kotonebot/kaa/sprites/{6abe81b7-1a04-44e9-b390-affe47799c46.png → 8e1ca316-39e6-4b9f-b497-84ad8848a3e1.png} +0 -0
- /kotonebot/kaa/sprites/{f14cc536-f38c-4568-9812-d3ed95e639d3.png → 8f675963-122a-4932-9f33-07dcdcb3a51a.png} +0 -0
- /kotonebot/kaa/sprites/{0df088e0-737f-44ec-b39f-76239d087382.png → 907dc373-e2f5-4959-84cb-d7971a7fefd8.png} +0 -0
- /kotonebot/kaa/sprites/{7d5b4d44-cb46-49fb-96be-edaab5396836.png → 9359526e-c5db-4d63-adc4-08b7d704b422.png} +0 -0
- /kotonebot/kaa/sprites/{2860421f-2db8-4850-9f0c-0aca8bbf16d0.png → 96178cef-4868-4378-9f9c-c08780def3d6.png} +0 -0
- /kotonebot/kaa/sprites/{a477c873-98bd-45f0-a9a6-324e714c69cd.png → 985ec867-bc7c-47c5-8fcd-75f736f27507.png} +0 -0
- /kotonebot/kaa/sprites/{669f9e7c-b665-47a8-b4f5-fb2748bf88af.png → 9c98c4e6-5335-4c8b-92fa-24ec440b055e.png} +0 -0
- /kotonebot/kaa/sprites/{1373ec63-fd98-4be9-a925-08144f1d5ba5.png → a015cea0-d2c7-439a-b942-d4d2dad276e5.png} +0 -0
- /kotonebot/kaa/sprites/{6fec0efa-336f-4726-b1d9-dc83c23b297d.png → a05abc9d-9f0e-4d83-94bb-4f0b1e061035.png} +0 -0
- /kotonebot/kaa/sprites/{957dbdc4-8367-4a59-9bfc-d35ea11af9b8.png → a2e41d98-dd91-4e51-ba57-4f8ddfd07cfa.png} +0 -0
- /kotonebot/kaa/sprites/{8e78e1f1-ea2f-4779-a8cd-56336f936c81.png → a402b549-a4dd-4ff3-ad51-2aa5f8e620af.png} +0 -0
- /kotonebot/kaa/sprites/{ff493975-0f30-468f-a3ea-17505a2b0385.png → a5efb332-8452-49d8-ade3-882bbd445553.png} +0 -0
- /kotonebot/kaa/sprites/{dfe2fc89-eaf7-4027-bb53-dd17a9741e01.png → a9f47d53-fbef-4cb0-9d2d-845ef3d1f719.png} +0 -0
- /kotonebot/kaa/sprites/{45ae4d17-1887-4f2e-8cce-e5a4cc42e10b.png → b10596fe-22fa-4284-ab4b-1d653d4f0f07.png} +0 -0
- /kotonebot/kaa/sprites/{9cc7997f-3aeb-4b20-ae42-468ebfbc851e.png → b326c50f-c88c-4b99-80a7-046053423a19.png} +0 -0
- /kotonebot/kaa/sprites/{6d706642-441f-4bcc-b377-0fce3484be22.png → b44085c7-9777-415a-aae1-aae47c2f7321.png} +0 -0
- /kotonebot/kaa/sprites/{54d57991-e5d0-4346-b1f8-63e429d8a3fe.png → b629f0a7-df4f-4407-908b-a031c70e8b6f.png} +0 -0
- /kotonebot/kaa/sprites/{63f28640-766f-4698-b556-f0a0cbac6a1a.png → b8165178-a9ce-491a-ba57-d25c7af8533f.png} +0 -0
- /kotonebot/kaa/sprites/{0c3cc689-90f5-4c29-be80-04e2f2ab799a.png → b8eeacac-5679-42b9-a5b9-138598c8c6b6.png} +0 -0
- /kotonebot/kaa/sprites/{f89cafb7-657b-4ab4-8917-1964b242ff2e.png → b9ad5832-889a-4a5f-adc0-5dfd55126d6a.png} +0 -0
- /kotonebot/kaa/sprites/{16576157-f99b-4a64-b41b-3ee712c2304e.png → bcb95d32-32e1-4251-83de-ef5164d22a48.png} +0 -0
- /kotonebot/kaa/sprites/{7e64561b-0dab-420a-8f79-45ed6801939b.png → c0ddf15a-a6f6-42f6-86b7-e17cde60b437.png} +0 -0
- /kotonebot/kaa/sprites/{6d4f414d-ccd8-4a25-aa32-1b77ce0035d6.png → c24e874e-6a3e-472c-8745-edf7cac88954.png} +0 -0
- /kotonebot/kaa/sprites/{2b87ed7c-b01b-4ea7-ab34-ec4e37e036c0.png → c362b54d-d535-4227-8612-9bfe11fd7891.png} +0 -0
- /kotonebot/kaa/sprites/{1e7924dc-9dd4-4d54-add8-b394c75af1c7.png → c51132e9-f45c-40c9-8026-7d70b13dca8b.png} +0 -0
- /kotonebot/kaa/sprites/{5663551f-5abc-4ef8-a3fd-ba57b20cb35c.png → c60bec93-231f-498d-b3c6-0a78c9b8f37f.png} +0 -0
- /kotonebot/kaa/sprites/{89d6407a-982c-4dbc-8dd7-89cf79429eec.png → c64ff0a8-9f96-4ca7-902b-4f7298c1c431.png} +0 -0
- /kotonebot/kaa/sprites/{bda40c0d-55a9-41ad-ba8a-e8aafc52608b.png → c819d999-fef6-47bb-a94f-055d616033c7.png} +0 -0
- /kotonebot/kaa/sprites/{1b8b2a66-fb45-41d7-8441-04425a47bdeb.png → c85e58d3-ec5a-43ea-aa2a-04ee28e9ea21.png} +0 -0
- /kotonebot/kaa/sprites/{b332eb23-6653-4d17-b779-2451a3b261ab.png → c883049b-187c-4685-a63a-cff516851a95.png} +0 -0
- /kotonebot/kaa/sprites/{60f292d1-6c2d-4cfb-af76-568610817722.png → c8f0cc49-5df8-4d7a-9ef6-2f021c2380a5.png} +0 -0
- /kotonebot/kaa/sprites/{7383d7d0-8ee2-4413-b590-51cff541cab3.png → c9a8d73f-f487-45ff-aa05-a61393d27f66.png} +0 -0
- /kotonebot/kaa/sprites/{817c22a3-ecfe-4949-a784-59342e61a880.png → cb4c357f-b01b-4b95-b928-5affdf4ae684.png} +0 -0
- /kotonebot/kaa/sprites/{dff3509c-f0ec-44e2-8e0b-3c6b465d45f3.png → cc0e38b2-c2ae-4fe1-adb0-6ea73a602563.png} +0 -0
- /kotonebot/kaa/sprites/{c7a33e9e-2f1c-4171-b1f7-7d7dcc1a7bd3.png → cdd40676-b49d-42ad-9972-7149aa13d9bb.png} +0 -0
- /kotonebot/kaa/sprites/{5d912f4e-c319-4d45-85e6-de6e684a6278.png → cf76eb98-44ff-406e-913a-503a0233b4cd.png} +0 -0
- /kotonebot/kaa/sprites/{7b195535-7362-492f-8d87-b8eed15c6fc0.png → d21117c2-3679-4bb0-a5b3-5af5d12d8d3e.png} +0 -0
- /kotonebot/kaa/sprites/{7d778f82-f908-4edb-9b12-d8460589ab11.png → d37bc673-9668-4b7e-bc9d-54495129dcc0.png} +0 -0
- /kotonebot/kaa/sprites/{08750a02-0881-443a-9c69-c90a2ccf2a63.png → dac24ef9-e53d-4d95-aa09-fa694606c3ab.png} +0 -0
- /kotonebot/kaa/sprites/{31271771-5149-447c-8fae-ccc308c0a838.png → dd084696-be33-4a00-b6df-b25131b78b40.png} +0 -0
- /kotonebot/kaa/sprites/{a35aed53-ff78-41f0-9a19-c69fda40aeef.png → de1e3c4a-f6ed-467a-889c-25c01a613477.png} +0 -0
- /kotonebot/kaa/sprites/{639465a0-3c63-4307-a72c-4f9e543d2095.png → e1eb4596-73a1-471e-892b-c83cfb706bb3.png} +0 -0
- /kotonebot/kaa/sprites/{91940a9e-2cfc-423a-86d6-49429c051ea0.png → e1ec3ee5-e73c-44b6-b173-ab1c9382cba4.png} +0 -0
- /kotonebot/kaa/sprites/{096d3df5-2c95-41dd-bcc3-2e3afab47420.png → e4d076bd-7824-4496-a498-d6dc01ad90ba.png} +0 -0
- /kotonebot/kaa/sprites/{50a9e2a0-1166-4f1d-ad15-d8493ce0e49e.png → e551148d-1b0d-4518-8711-4161863c93ef.png} +0 -0
- /kotonebot/kaa/sprites/{45671af1-d731-4bb1-83f7-c9d23348308f.png → e61d855e-84d3-4a0e-b996-43fce262c2c3.png} +0 -0
- /kotonebot/kaa/sprites/{608217f6-b880-42ab-9be9-b4d56df87be2.png → e795f9c5-fdda-4fbc-8d38-a92512a590f4.png} +0 -0
- /kotonebot/kaa/sprites/{7d4d708f-c629-4f52-a0af-11aee7fcb878.png → eb446c0a-1dd2-4693-838e-98a76c02eac5.png} +0 -0
- /kotonebot/kaa/sprites/{35e6ad3a-8020-4619-b8ec-6506a8aa51fc.png → eb4bde42-de6e-4c62-8dc4-8885efc834a8.png} +0 -0
- /kotonebot/kaa/sprites/{9a5e7bc9-830f-4b01-8f94-6d4f589754c0.png → ec547e11-4952-4089-9744-1e89bd44389c.png} +0 -0
- /kotonebot/kaa/sprites/{40e3dac1-74fb-4501-a20f-18422d070554.png → ed4a8cff-b3df-4d7a-bfc4-8da776a8e548.png} +0 -0
- /kotonebot/kaa/sprites/{39d57097-6839-4030-bb51-24d6cd880897.png → efc08f1c-e78f-4df8-bff9-b45cde37f9dd.png} +0 -0
- /kotonebot/kaa/sprites/{9f56889b-0251-4ec1-8ccb-5cfd3bddd396.png → f02d997a-a3a8-4e18-96f1-e4176c14a5e0.png} +0 -0
- /kotonebot/kaa/sprites/{97bbc57d-780a-40f4-8fe1-bcaef697b601.png → f5e2e4e1-40be-481d-9293-9d47a5bce7cc.png} +0 -0
- /kotonebot/kaa/sprites/{bee4fbe2-365f-4145-b8f4-87c2c460cd71.png → f62dd2a2-bac6-4dc8-9a19-3e995ab80a3b.png} +0 -0
- /kotonebot/kaa/sprites/{666b1e8c-19fb-49e9-a761-1069f635f450.png → f64cfd95-8c87-47e6-96c3-8b7df2624ae3.png} +0 -0
- /kotonebot/kaa/sprites/{847cb208-a71c-42e3-97f1-18d4aa886b41.png → f8bcbc2c-9831-4e8d-9ffd-e532aa3193c3.png} +0 -0
- /kotonebot/kaa/sprites/{9e5c5ffc-450d-4ddc-a4ae-27c33267b056.png → fb50c7d1-b983-4e36-a998-ea17a1bdb18b.png} +0 -0
- /kotonebot/kaa/sprites/{f1a60528-1f35-451f-92e3-b1d5407e0f4e.png → fb97d804-42e4-45e2-8b22-01d16b5d0b6b.png} +0 -0
- /kotonebot/kaa/sprites/{a3140a53-ffb4-4094-a39c-240e9fe42308.png → fe337541-a6a3-4325-97b4-2bf27f51f39f.png} +0 -0
- /kotonebot/kaa/sprites/{1a0f2a10-c23b-47c7-aa7e-89b606f4d8ae.png → ffd3c6b7-cac2-4d45-ae37-792404ea2b40.png} +0 -0
- {ksaa-2025.5.16.1.dist-info → ksaa-2025.5.23.1.dist-info}/entry_points.txt +0 -0
- {ksaa-2025.5.16.1.dist-info → ksaa-2025.5.23.1.dist-info}/top_level.txt +0 -0
kotonebot/client/device.py
CHANGED
|
@@ -8,10 +8,10 @@ from adbutils import adb
|
|
|
8
8
|
from cv2.typing import MatLike
|
|
9
9
|
from adbutils._device import AdbDevice as AdbUtilsDevice
|
|
10
10
|
|
|
11
|
+
from ..backend.debug import result
|
|
11
12
|
from kotonebot.backend.core import HintBox
|
|
12
|
-
from kotonebot.
|
|
13
|
+
from kotonebot.primitives import Rect, Point, is_point
|
|
13
14
|
from .protocol import ClickableObjectProtocol, Commandable, Touchable, Screenshotable
|
|
14
|
-
from ..backend.debug import result
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -119,14 +119,7 @@ class Device:
|
|
|
119
119
|
点击屏幕上的某个点
|
|
120
120
|
"""
|
|
121
121
|
...
|
|
122
|
-
|
|
123
|
-
@overload
|
|
124
|
-
def click(self, hint_box: HintBox) -> None:
|
|
125
|
-
"""
|
|
126
|
-
点击屏幕上的某个矩形区域
|
|
127
|
-
"""
|
|
128
|
-
...
|
|
129
|
-
|
|
122
|
+
|
|
130
123
|
@overload
|
|
131
124
|
def click(self, rect: Rect) -> None:
|
|
132
125
|
"""
|
|
@@ -136,7 +129,6 @@ class Device:
|
|
|
136
129
|
|
|
137
130
|
@overload
|
|
138
131
|
def click(self, clickable: ClickableObjectProtocol) -> None:
|
|
139
|
-
|
|
140
132
|
"""
|
|
141
133
|
点击屏幕上的某个可点击对象
|
|
142
134
|
"""
|
|
@@ -147,9 +139,7 @@ class Device:
|
|
|
147
139
|
arg2 = args[1] if len(args) > 1 else None
|
|
148
140
|
if arg1 is None:
|
|
149
141
|
self.__click_last()
|
|
150
|
-
elif isinstance(arg1,
|
|
151
|
-
self.__click_hint_box(arg1)
|
|
152
|
-
elif is_rect(arg1):
|
|
142
|
+
elif isinstance(arg1, Rect):
|
|
153
143
|
self.__click_rect(arg1)
|
|
154
144
|
elif is_point(arg1):
|
|
155
145
|
self.__click_point_tuple(arg1)
|
|
@@ -167,8 +157,8 @@ class Device:
|
|
|
167
157
|
|
|
168
158
|
def __click_rect(self, rect: Rect) -> None:
|
|
169
159
|
# 从矩形中心的 60% 内部随机选择一点
|
|
170
|
-
x = rect
|
|
171
|
-
y = rect
|
|
160
|
+
x = rect.x1 + rect.w // 2 + np.random.randint(-int(rect.w * 0.3), int(rect.w * 0.3))
|
|
161
|
+
y = rect.y1 + rect.h // 2 + np.random.randint(-int(rect.h * 0.3), int(rect.h * 0.3))
|
|
172
162
|
x = int(x)
|
|
173
163
|
y = int(y)
|
|
174
164
|
self.click(x, y)
|
|
@@ -196,9 +186,6 @@ class Device:
|
|
|
196
186
|
def __click_clickable(self, clickable: ClickableObjectProtocol) -> None:
|
|
197
187
|
self.click(clickable.rect)
|
|
198
188
|
|
|
199
|
-
def __click_hint_box(self, hint_box: HintBox) -> None:
|
|
200
|
-
self.click(hint_box.rect)
|
|
201
|
-
|
|
202
189
|
def click_center(self) -> None:
|
|
203
190
|
"""
|
|
204
191
|
点击屏幕中心。
|
|
@@ -234,7 +221,7 @@ class Device:
|
|
|
234
221
|
def double_click(self, *args, **kwargs) -> None:
|
|
235
222
|
from kotonebot import sleep
|
|
236
223
|
arg0 = args[0]
|
|
237
|
-
if
|
|
224
|
+
if isinstance(arg0, Rect) or isinstance(arg0, ClickableObjectProtocol):
|
|
238
225
|
rect = arg0
|
|
239
226
|
interval = kwargs.get('interval', 0.4)
|
|
240
227
|
self.click(rect)
|
kotonebot/client/factory.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from
|
|
1
|
+
from time import sleep
|
|
2
2
|
from typing import Literal
|
|
3
3
|
|
|
4
4
|
from .implements.adb import AdbImpl
|
|
@@ -15,12 +15,38 @@ DeviceImpl = Literal['adb', 'adb_raw', 'uiautomator2', 'windows', 'remote_window
|
|
|
15
15
|
def create_device(
|
|
16
16
|
addr: str,
|
|
17
17
|
impl: DeviceImpl,
|
|
18
|
+
*,
|
|
19
|
+
connect: bool = True,
|
|
20
|
+
disconnect: bool = True,
|
|
21
|
+
device_serial: str | None = None,
|
|
22
|
+
timeout: float = 180,
|
|
18
23
|
) -> Device:
|
|
24
|
+
"""
|
|
25
|
+
根据指定的实现方式创建 Device 实例。
|
|
26
|
+
|
|
27
|
+
:param addr: 设备地址,如 `127.0.0.1:5555`。
|
|
28
|
+
仅当通过无线方式连接 Android 设备,或者使用 `remote_windows` 时有效。
|
|
29
|
+
:param impl: 实现方式。
|
|
30
|
+
:param connect: 是否在创建时连接设备,默认为 True。
|
|
31
|
+
仅对 ADB-based 的实现方式有效。
|
|
32
|
+
:param disconnect: 是否在连接前先断开设备,默认为 True。
|
|
33
|
+
仅对 ADB-based 的实现方式有效。
|
|
34
|
+
:param device_serial: 设备序列号,默认为 None。
|
|
35
|
+
若为非 None,则当存在多个设备时通过该值判断是否为目标设备。
|
|
36
|
+
仅对 ADB-based 的实现方式有效。
|
|
37
|
+
:param timeout: 连接超时时间,默认为 180 秒。
|
|
38
|
+
仅对 ADB-based 的实现方式有效。
|
|
39
|
+
"""
|
|
19
40
|
if impl in ['adb', 'adb_raw', 'uiautomator2']:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
if disconnect:
|
|
42
|
+
adb.disconnect(addr)
|
|
43
|
+
if connect:
|
|
44
|
+
result = adb.connect(addr)
|
|
45
|
+
if 'cannot connect to' in result:
|
|
46
|
+
raise ValueError(result)
|
|
47
|
+
serial = device_serial or addr
|
|
48
|
+
adb.wait_for(serial, timeout=timeout)
|
|
49
|
+
d = [d for d in adb.device_list() if d.serial == serial]
|
|
24
50
|
if len(d) == 0:
|
|
25
51
|
raise ValueError(f"Device {addr} not found")
|
|
26
52
|
d = d[0]
|
|
@@ -55,4 +81,6 @@ def create_device(
|
|
|
55
81
|
remote_impl = RemoteWindowsImpl(device, host, port)
|
|
56
82
|
device._touch = remote_impl
|
|
57
83
|
device._screenshot = remote_impl
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError(f"Unsupported device implementation: {impl}")
|
|
58
86
|
return device
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
from .protocol import HostProtocol, Instance
|
|
2
2
|
from .custom import CustomInstance, create as create_custom
|
|
3
|
+
from .mumu12_host import Mumu12Host, Mumu12Instance
|
|
4
|
+
from .leidian_host import LeidianHost, LeidianInstance
|
|
3
5
|
|
|
4
|
-
__all__ = [
|
|
6
|
+
__all__ = [
|
|
7
|
+
'HostProtocol', 'Instance',
|
|
8
|
+
'CustomInstance', 'create_custom',
|
|
9
|
+
'Mumu12Host', 'Mumu12Instance',
|
|
10
|
+
'LeidianHost', 'LeidianInstance'
|
|
11
|
+
]
|
kotonebot/client/host/custom.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Optional, ParamSpec, TypeVar, TypeGuard
|
|
|
6
6
|
from typing_extensions import override
|
|
7
7
|
|
|
8
8
|
from kotonebot import logging
|
|
9
|
+
from kotonebot.client.device import Device
|
|
9
10
|
|
|
10
11
|
logger = logging.getLogger(__name__)
|
|
11
12
|
|
|
@@ -55,6 +56,10 @@ class CustomInstance(Instance):
|
|
|
55
56
|
else:
|
|
56
57
|
return False
|
|
57
58
|
|
|
59
|
+
@override
|
|
60
|
+
def refresh(self):
|
|
61
|
+
pass
|
|
62
|
+
|
|
58
63
|
def __repr__(self) -> str:
|
|
59
64
|
return f'CustomInstance(#{self.id}# at "{self.exe_path}" with {self.adb_ip}:{self.adb_port})'
|
|
60
65
|
|
|
@@ -63,8 +68,8 @@ def _type_check(ins: Instance) -> CustomInstance:
|
|
|
63
68
|
raise ValueError(f'Instance {ins} is not a CustomInstance')
|
|
64
69
|
return ins
|
|
65
70
|
|
|
66
|
-
def create(exe_path: str, adb_ip: str, adb_port: int,
|
|
67
|
-
return CustomInstance(exe_path, emulator_args=emulator_args, id='custom', name='Custom', adb_ip=adb_ip, adb_port=adb_port,
|
|
71
|
+
def create(exe_path: str, adb_ip: str, adb_port: int, adb_name: str, emulator_args: str = "") -> CustomInstance:
|
|
72
|
+
return CustomInstance(exe_path, emulator_args=emulator_args, id='custom', name='Custom', adb_ip=adb_ip, adb_port=adb_port, adb_name=adb_name)
|
|
68
73
|
|
|
69
74
|
|
|
70
75
|
if __name__ == '__main__':
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing_extensions import override
|
|
5
|
+
|
|
6
|
+
from kotonebot import logging
|
|
7
|
+
from kotonebot.client import DeviceImpl, create_device
|
|
8
|
+
from kotonebot.client.device import Device
|
|
9
|
+
from kotonebot.util import Countdown, Interval
|
|
10
|
+
from .protocol import HostProtocol, Instance, copy_type
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
if os.name == 'nt':
|
|
15
|
+
from ...interop.win.reg import read_reg
|
|
16
|
+
else:
|
|
17
|
+
def read_reg(key, subkey, name, *, default=None, **kwargs):
|
|
18
|
+
"""Stub for read_reg on non-Windows platforms."""
|
|
19
|
+
return default
|
|
20
|
+
|
|
21
|
+
class LeidianInstance(Instance):
|
|
22
|
+
@copy_type(Instance.__init__)
|
|
23
|
+
def __init__(self, *args, **kwargs):
|
|
24
|
+
super().__init__(*args, **kwargs)
|
|
25
|
+
self._args = args
|
|
26
|
+
self.index: int | None = None
|
|
27
|
+
self.is_running: bool = False
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
def refresh(self):
|
|
31
|
+
ins = LeidianHost.query(id=self.id)
|
|
32
|
+
assert isinstance(ins, LeidianInstance), f'Expected LeidianInstance, got {type(ins)}'
|
|
33
|
+
if ins is not None:
|
|
34
|
+
self.adb_port = ins.adb_port
|
|
35
|
+
self.adb_ip = ins.adb_ip
|
|
36
|
+
self.adb_name = ins.adb_name
|
|
37
|
+
self.is_running = ins.is_running
|
|
38
|
+
logger.debug('Refreshed Leidian instance: %s', repr(ins))
|
|
39
|
+
|
|
40
|
+
@override
|
|
41
|
+
def start(self):
|
|
42
|
+
if self.running():
|
|
43
|
+
logger.warning('Instance is already running.')
|
|
44
|
+
return
|
|
45
|
+
logger.info('Starting Leidian instance %s', self)
|
|
46
|
+
LeidianHost._invoke_manager(['launch', '--index', str(self.index)])
|
|
47
|
+
self.refresh()
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
def stop(self):
|
|
51
|
+
if not self.running():
|
|
52
|
+
logger.warning('Instance is not running.')
|
|
53
|
+
return
|
|
54
|
+
logger.info('Stopping Leidian instance id=%s name=%s...', self.id, self.name)
|
|
55
|
+
LeidianHost._invoke_manager(['quit', '--index', str(self.index)])
|
|
56
|
+
self.refresh()
|
|
57
|
+
|
|
58
|
+
@override
|
|
59
|
+
def wait_available(self, timeout: float = 180):
|
|
60
|
+
cd = Countdown(timeout)
|
|
61
|
+
it = Interval(5)
|
|
62
|
+
while not cd.expired() and not self.running():
|
|
63
|
+
it.wait()
|
|
64
|
+
if not self.running():
|
|
65
|
+
raise TimeoutError(f'Leidian instance "{self.name}" is not available.')
|
|
66
|
+
|
|
67
|
+
@override
|
|
68
|
+
def running(self) -> bool:
|
|
69
|
+
result = LeidianHost._invoke_manager(['isrunning', '--index', str(self.index)])
|
|
70
|
+
return result.strip() == 'running'
|
|
71
|
+
|
|
72
|
+
@override
|
|
73
|
+
def create_device(self, impl: DeviceImpl, *, timeout: float = 180) -> Device:
|
|
74
|
+
if self.adb_port is None:
|
|
75
|
+
raise ValueError("ADB port is not set and is required.")
|
|
76
|
+
return create_device(
|
|
77
|
+
addr=f'{self.adb_ip}:{self.adb_port}',
|
|
78
|
+
impl=impl,
|
|
79
|
+
device_serial=self.adb_name,
|
|
80
|
+
connect=False,
|
|
81
|
+
timeout=timeout
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
class LeidianHost(HostProtocol):
|
|
85
|
+
@staticmethod
|
|
86
|
+
@lru_cache(maxsize=1)
|
|
87
|
+
def _read_install_path() -> str | None:
|
|
88
|
+
"""
|
|
89
|
+
从注册表中读取雷电模拟器的安装路径。
|
|
90
|
+
|
|
91
|
+
:return: 安装路径,如果未找到则返回 None。
|
|
92
|
+
"""
|
|
93
|
+
if os.name != 'nt':
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
icon_path = read_reg('HKCU', r'Software\leidian\LDPlayer9', 'DisplayIcon', default=None)
|
|
98
|
+
if icon_path and isinstance(icon_path, str):
|
|
99
|
+
icon_path = icon_path.replace('"', '')
|
|
100
|
+
path = os.path.dirname(icon_path)
|
|
101
|
+
logger.debug('Leidian installation path (from DisplayIcon): %s', path)
|
|
102
|
+
return path
|
|
103
|
+
install_dir = read_reg('HKCU', r'Software\leidian\LDPlayer9', 'InstallDir', default=None)
|
|
104
|
+
if install_dir and isinstance(install_dir, str):
|
|
105
|
+
install_dir = install_dir.replace('"', '')
|
|
106
|
+
logger.debug('Leidian installation path (from InstallDir): %s', install_dir)
|
|
107
|
+
return install_dir
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.error(f'Failed to read Leidian installation path from registry: {e}')
|
|
110
|
+
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _invoke_manager(args: list[str]) -> str:
|
|
115
|
+
"""
|
|
116
|
+
调用 ldconsole.exe。
|
|
117
|
+
|
|
118
|
+
参考文档:https://www.ldmnq.com/forum/30.html,以及命令行帮助。
|
|
119
|
+
另外还有个 ld.exe,封装了 adb.exe,可以直接执行 adb 命令。(https://www.ldmnq.com/forum/9178.html)
|
|
120
|
+
|
|
121
|
+
:param args: 命令行参数列表。
|
|
122
|
+
:return: 命令执行的输出。
|
|
123
|
+
"""
|
|
124
|
+
install_path = LeidianHost._read_install_path()
|
|
125
|
+
if install_path is None:
|
|
126
|
+
raise RuntimeError('Leidian is not installed.')
|
|
127
|
+
manager_path = os.path.join(install_path, 'ldconsole.exe')
|
|
128
|
+
logger.debug('ldconsole execute: %s', repr(args))
|
|
129
|
+
output = subprocess.run(
|
|
130
|
+
[manager_path] + args,
|
|
131
|
+
capture_output=True,
|
|
132
|
+
text=True,
|
|
133
|
+
# encoding='utf-8', # 居然不是 utf-8 编码
|
|
134
|
+
# https://stackoverflow.com/questions/6011235/run-a-program-from-python-and-have-it-continue-to-run-after-the-script-is-kille
|
|
135
|
+
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
|
136
|
+
)
|
|
137
|
+
if output.returncode != 0:
|
|
138
|
+
raise RuntimeError(f'Failed to invoke ldconsole: {output.stderr}')
|
|
139
|
+
return output.stdout
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def installed() -> bool:
|
|
143
|
+
return LeidianHost._read_install_path() is not None
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def list() -> list[Instance]:
|
|
147
|
+
output = LeidianHost._invoke_manager(['list2'])
|
|
148
|
+
instances = []
|
|
149
|
+
|
|
150
|
+
# 解析 list2 命令的输出
|
|
151
|
+
# 格式: 索引,标题,顶层窗口句柄,绑定窗口句柄,是否进入android,进程PID,VBox进程PID
|
|
152
|
+
for line in output.strip().split('\n'):
|
|
153
|
+
if not line:
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
parts = line.split(',')
|
|
157
|
+
if len(parts) < 5:
|
|
158
|
+
logger.warning(f'Invalid list2 output line: {line}')
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
index = parts[0]
|
|
162
|
+
name = parts[1]
|
|
163
|
+
is_android_started = parts[4] == '1'
|
|
164
|
+
# 端口号规则 https://help.ldmnq.com/docs/LD9adbserver#a67730c2e7e2e0400d40bcab37d0e0cf
|
|
165
|
+
adb_port = 5554 + (int(index) * 2)
|
|
166
|
+
|
|
167
|
+
instance = LeidianInstance(
|
|
168
|
+
id=index,
|
|
169
|
+
name=name,
|
|
170
|
+
adb_port=adb_port,
|
|
171
|
+
adb_ip='127.0.0.1',
|
|
172
|
+
adb_name=f'emulator-{adb_port}'
|
|
173
|
+
)
|
|
174
|
+
instance.index = int(index)
|
|
175
|
+
instance.is_running = is_android_started
|
|
176
|
+
logger.debug('Leidian instance: %s', repr(instance))
|
|
177
|
+
instances.append(instance)
|
|
178
|
+
|
|
179
|
+
return instances
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def query(*, id: str) -> Instance | None:
|
|
183
|
+
instances = LeidianHost.list()
|
|
184
|
+
for instance in instances:
|
|
185
|
+
if instance.id == id:
|
|
186
|
+
return instance
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
if __name__ == '__main__':
|
|
190
|
+
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
|
191
|
+
print(LeidianHost._read_install_path())
|
|
192
|
+
print(LeidianHost.installed())
|
|
193
|
+
print(LeidianHost.list())
|
|
194
|
+
print(ins:=LeidianHost.query(id='0'))
|
|
195
|
+
assert isinstance(ins, LeidianInstance)
|
|
196
|
+
ins.start()
|
|
197
|
+
ins.wait_available()
|
|
198
|
+
print('status', ins.running(), ins.adb_port, ins.adb_ip)
|
|
199
|
+
# ins.stop()
|
|
200
|
+
# print('status', ins.running(), ins.adb_port, ins.adb_ip)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any
|
|
6
|
+
from typing_extensions import override
|
|
7
|
+
|
|
8
|
+
from kotonebot import logging
|
|
9
|
+
from kotonebot.client import DeviceImpl, Device
|
|
10
|
+
from kotonebot.util import Countdown, Interval
|
|
11
|
+
from .protocol import HostProtocol, Instance, copy_type
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
if os.name == 'nt':
|
|
16
|
+
from ...interop.win.reg import read_reg
|
|
17
|
+
else:
|
|
18
|
+
def read_reg(key, subkey, name, *, default=None, **kwargs):
|
|
19
|
+
"""Stub for read_reg on non-Windows platforms."""
|
|
20
|
+
return default
|
|
21
|
+
|
|
22
|
+
class Mumu12Instance(Instance):
|
|
23
|
+
@copy_type(Instance.__init__)
|
|
24
|
+
def __init__(self, *args, **kwargs):
|
|
25
|
+
super().__init__(*args, **kwargs)
|
|
26
|
+
self._args = args
|
|
27
|
+
self.index: int | None = None
|
|
28
|
+
self.is_android_started: bool = False
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
def refresh(self):
|
|
32
|
+
ins = Mumu12Host.query(id=self.id)
|
|
33
|
+
assert isinstance(ins, Mumu12Instance), f'Expected Mumu12Instance, got {type(ins)}'
|
|
34
|
+
if ins is not None:
|
|
35
|
+
self.adb_port = ins.adb_port
|
|
36
|
+
self.adb_ip = ins.adb_ip
|
|
37
|
+
self.adb_name = ins.adb_name
|
|
38
|
+
self.is_android_started = ins.is_android_started
|
|
39
|
+
logger.debug('Refreshed MuMu12 instance: %s', repr(ins))
|
|
40
|
+
|
|
41
|
+
@override
|
|
42
|
+
def start(self):
|
|
43
|
+
if self.running():
|
|
44
|
+
logger.warning('Instance is already running.')
|
|
45
|
+
return
|
|
46
|
+
logger.info('Starting MuMu12 instance %s', self)
|
|
47
|
+
Mumu12Host._invoke_manager(['control', '-v', self.id, 'launch'])
|
|
48
|
+
self.refresh()
|
|
49
|
+
|
|
50
|
+
@override
|
|
51
|
+
def stop(self):
|
|
52
|
+
if not self.running():
|
|
53
|
+
logger.warning('Instance is not running.')
|
|
54
|
+
return
|
|
55
|
+
logger.info('Stopping MuMu12 instance id=%s name=%s...', self.id, self.name)
|
|
56
|
+
Mumu12Host._invoke_manager(['control', '-v', self.id, 'shutdown'])
|
|
57
|
+
self.refresh()
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
def wait_available(self, timeout: float = 180):
|
|
61
|
+
cd = Countdown(timeout)
|
|
62
|
+
it = Interval(5)
|
|
63
|
+
while not cd.expired() and not self.running():
|
|
64
|
+
it.wait()
|
|
65
|
+
self.refresh()
|
|
66
|
+
if not self.running():
|
|
67
|
+
raise TimeoutError(f'MuMu12 instance "{self.name}" is not available.')
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
def running(self) -> bool:
|
|
71
|
+
return self.is_android_started
|
|
72
|
+
|
|
73
|
+
class Mumu12Host(HostProtocol):
|
|
74
|
+
@staticmethod
|
|
75
|
+
@lru_cache(maxsize=1)
|
|
76
|
+
def _read_install_path() -> str | None:
|
|
77
|
+
"""
|
|
78
|
+
Reads the installation path (DisplayIcon) of MuMu Player 12 from the registry.
|
|
79
|
+
|
|
80
|
+
:return: The path to the display icon if found, otherwise None.
|
|
81
|
+
"""
|
|
82
|
+
if os.name != 'nt':
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
uninstall_subkeys = [
|
|
86
|
+
r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayer-12.0',
|
|
87
|
+
# TODO: 支持国际版 MuMu
|
|
88
|
+
# r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayerGlobal-12.0'
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
for subkey in uninstall_subkeys:
|
|
92
|
+
icon_path = read_reg('HKLM', subkey, 'DisplayIcon', default=None)
|
|
93
|
+
if icon_path and isinstance(icon_path, str):
|
|
94
|
+
icon_path = icon_path.replace('"', '')
|
|
95
|
+
path = os.path.dirname(icon_path)
|
|
96
|
+
logger.debug('MuMu Player 12 installation path: %s', path)
|
|
97
|
+
return path
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _invoke_manager(args: list[str]) -> str:
|
|
102
|
+
"""
|
|
103
|
+
调用 MuMuManager.exe。
|
|
104
|
+
|
|
105
|
+
:param args: 命令行参数列表。
|
|
106
|
+
:return: 命令执行的输出。
|
|
107
|
+
"""
|
|
108
|
+
install_path = Mumu12Host._read_install_path()
|
|
109
|
+
if install_path is None:
|
|
110
|
+
raise RuntimeError('MuMu Player 12 is not installed.')
|
|
111
|
+
manager_path = os.path.join(install_path, 'MuMuManager.exe')
|
|
112
|
+
logger.debug('MuMuManager execute: %s', repr(args))
|
|
113
|
+
output = subprocess.run(
|
|
114
|
+
[manager_path] + args,
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True,
|
|
117
|
+
encoding='utf-8',
|
|
118
|
+
# https://stackoverflow.com/questions/6011235/run-a-program-from-python-and-have-it-continue-to-run-after-the-script-is-kille
|
|
119
|
+
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
|
120
|
+
)
|
|
121
|
+
if output.returncode != 0:
|
|
122
|
+
# raise RuntimeError(f'Failed to invoke MuMuManager: {output.stderr}')
|
|
123
|
+
logger.warning('Failed to invoke MuMuManager: %s', output.stderr)
|
|
124
|
+
return output.stdout
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def installed() -> bool:
|
|
128
|
+
return Mumu12Host._read_install_path() is not None
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def list() -> list[Instance]:
|
|
132
|
+
output = Mumu12Host._invoke_manager(['info', '-v', 'all'])
|
|
133
|
+
logger.debug('MuMuManager.exe output: %s', output)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
data: dict[str, dict[str, Any]] = json.loads(output)
|
|
137
|
+
if 'name' in data.keys():
|
|
138
|
+
# 这里有个坑:
|
|
139
|
+
# 如果只有一个实例,返回的 JSON 结构是单个对象而不是数组
|
|
140
|
+
data = { '0': data }
|
|
141
|
+
instances = []
|
|
142
|
+
for index, instance_data in data.items():
|
|
143
|
+
instance = Mumu12Instance(
|
|
144
|
+
id=index,
|
|
145
|
+
name=instance_data['name'],
|
|
146
|
+
adb_port=instance_data.get('adb_port'),
|
|
147
|
+
adb_ip=instance_data.get('adb_host_ip', '127.0.0.1'),
|
|
148
|
+
adb_name=None
|
|
149
|
+
)
|
|
150
|
+
instance.index = int(index)
|
|
151
|
+
instance.is_android_started = instance_data.get('is_android_started', False)
|
|
152
|
+
logger.debug('Mumu12 instance: %s', repr(instance))
|
|
153
|
+
instances.append(instance)
|
|
154
|
+
return instances
|
|
155
|
+
except json.JSONDecodeError as e:
|
|
156
|
+
raise RuntimeError(f'Failed to parse output: {e}')
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def query(*, id: str) -> Instance | None:
|
|
160
|
+
instances = Mumu12Host.list()
|
|
161
|
+
for instance in instances:
|
|
162
|
+
if instance.id == id:
|
|
163
|
+
return instance
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
if __name__ == '__main__':
|
|
167
|
+
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
|
168
|
+
print(Mumu12Host._read_install_path())
|
|
169
|
+
print(Mumu12Host.installed())
|
|
170
|
+
print(Mumu12Host.list())
|
|
171
|
+
print(ins:=Mumu12Host.query(id='2'))
|
|
172
|
+
assert isinstance(ins, Mumu12Instance)
|
|
173
|
+
ins.start()
|
|
174
|
+
ins.wait_available()
|
|
175
|
+
print('status', ins.running(), ins.adb_port, ins.adb_ip)
|
|
176
|
+
ins.stop()
|
|
177
|
+
print('status', ins.running(), ins.adb_port, ins.adb_ip)
|