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.
Files changed (191) hide show
  1. kotonebot/__init__.py +0 -2
  2. kotonebot/backend/bot.py +19 -34
  3. kotonebot/backend/color.py +20 -17
  4. kotonebot/backend/context/context.py +41 -8
  5. kotonebot/backend/core.py +7 -30
  6. kotonebot/backend/dispatch.py +1 -1
  7. kotonebot/backend/image.py +26 -26
  8. kotonebot/backend/ocr.py +18 -17
  9. kotonebot/backend/preprocessor.py +0 -1
  10. kotonebot/client/__init__.py +8 -1
  11. kotonebot/client/device.py +7 -20
  12. kotonebot/client/factory.py +33 -5
  13. kotonebot/client/host/__init__.py +8 -1
  14. kotonebot/client/host/custom.py +7 -2
  15. kotonebot/client/host/leidian_host.py +200 -0
  16. kotonebot/client/host/mumu12_host.py +177 -0
  17. kotonebot/client/host/protocol.py +68 -38
  18. kotonebot/client/implements/adb.py +1 -1
  19. kotonebot/client/protocol.py +1 -1
  20. kotonebot/config/base_config.py +8 -3
  21. kotonebot/interop/win/reg.py +37 -0
  22. kotonebot/kaa/common.py +18 -2
  23. kotonebot/kaa/game_ui/badge.py +6 -6
  24. kotonebot/kaa/game_ui/common.py +6 -6
  25. kotonebot/kaa/game_ui/commu_event_buttons.py +4 -3
  26. kotonebot/kaa/game_ui/idols_overview.py +6 -6
  27. kotonebot/kaa/game_ui/schedule.py +162 -0
  28. kotonebot/kaa/game_ui/scrollable.py +7 -6
  29. kotonebot/kaa/kaa_context.py +8 -0
  30. kotonebot/kaa/main/cli.py +7 -0
  31. kotonebot/kaa/main/dmm_host.py +53 -0
  32. kotonebot/kaa/main/gr.py +508 -298
  33. kotonebot/kaa/main/kaa.py +77 -1
  34. kotonebot/kaa/metadata.py +26 -0
  35. kotonebot/kaa/resources/__pycache__/__init__.cpython-310.pyc +0 -0
  36. kotonebot/kaa/resources/game.db +0 -0
  37. kotonebot/kaa/resources/game_ver.txt +0 -0
  38. kotonebot/kaa/sprites/4503db6b-7224-4b81-9971-e7cfa56e10f2.png +0 -0
  39. kotonebot/kaa/tasks/R.py +137 -133
  40. kotonebot/kaa/tasks/daily/contest.py +123 -89
  41. kotonebot/kaa/tasks/daily/mission_reward.py +4 -4
  42. kotonebot/kaa/tasks/end_game.py +2 -9
  43. kotonebot/kaa/tasks/produce/cards.py +7 -6
  44. kotonebot/kaa/tasks/produce/common.py +16 -12
  45. kotonebot/kaa/tasks/produce/in_purodyuusu.py +5 -5
  46. kotonebot/kaa/tasks/produce/p_drink.py +4 -3
  47. kotonebot/kaa/tasks/produce/produce.py +5 -3
  48. kotonebot/kaa/tasks/start_game.py +2 -2
  49. kotonebot/primitives/__init__.py +17 -0
  50. kotonebot/primitives/geometry.py +290 -0
  51. kotonebot/primitives/visual.py +63 -0
  52. kotonebot/util.py +20 -20
  53. {ksaa-2025.5.16.1.dist-info → ksaa-2025.5.23.1.dist-info}/METADATA +1 -1
  54. {ksaa-2025.5.16.1.dist-info → ksaa-2025.5.23.1.dist-info}/RECORD +191 -183
  55. {ksaa-2025.5.16.1.dist-info → ksaa-2025.5.23.1.dist-info}/WHEEL +1 -1
  56. /kotonebot/{client/host/mumu_host.py → interop/win/__init__.py} +0 -0
  57. /kotonebot/kaa/sprites/{7971b595-44d3-4ce7-91ae-a8e5a25c543b.png → 02ff2b86-db5a-4850-ac98-4288a3f71ed8.png} +0 -0
  58. /kotonebot/kaa/sprites/{bc64dd37-48ba-45a5-af6d-457a7fb2c9fd.png → 04422a5d-2499-40fd-a08f-095ecbd051a8.png} +0 -0
  59. /kotonebot/kaa/sprites/{17da97eb-93c5-438f-8688-0be5b0756019.png → 0672e1c6-6abc-452c-82b3-af6eb7454cfa.png} +0 -0
  60. /kotonebot/kaa/sprites/{5089ea64-3f32-4520-a252-4ee278815cdd.png → 07960be4-b575-4f7f-be64-7e344dc118e0.png} +0 -0
  61. /kotonebot/kaa/sprites/{0f67c0a6-45d0-4953-b1a9-b5eb2c916173.png → 09cd4336-7d2a-4bce-9d08-8ac64a2bdcc3.png} +0 -0
  62. /kotonebot/kaa/sprites/{5ed9bef4-ab11-4186-9b21-a479eec687ce.png → 09fda5a8-c546-48f0-ae29-ebe4f4208d8b.png} +0 -0
  63. /kotonebot/kaa/sprites/{7dec3bd9-f8ee-430f-af54-0c03c4c91546.png → 0b5c2d3f-28b1-456f-a452-05ca006f8b33.png} +0 -0
  64. /kotonebot/kaa/sprites/{76b0e3b3-3438-4e88-aca8-da18c8e50ddd.png → 0c359f2a-72d4-4203-9ca3-968a1948eee8.png} +0 -0
  65. /kotonebot/kaa/sprites/{9e110778-1b66-4700-b462-64c1dea368da.png → 0ecc3e37-189c-40ad-9ee6-fb576b72a13b.png} +0 -0
  66. /kotonebot/kaa/sprites/{bbcb97e2-89b7-4de2-b480-a0442fc33b1e.png → 11c2ff97-3303-4d6d-8e4f-6da2148bbe20.png} +0 -0
  67. /kotonebot/kaa/sprites/{36d712ce-9227-4aef-86ee-4f78415b0c80.png → 128886a7-7629-4158-9538-3619542f775b.png} +0 -0
  68. /kotonebot/kaa/sprites/{ba07fa5e-3451-4520-8b10-0b240e5decec.png → 13a85dbd-e226-47cc-b6f5-c0d3b40654f5.png} +0 -0
  69. /kotonebot/kaa/sprites/{f71df091-fc71-48d4-8124-b3c46f4c4436.png → 13c8fa4a-0ae5-4ee1-a433-20f6c724466e.png} +0 -0
  70. /kotonebot/kaa/sprites/{02e69560-ec42-4453-b771-8600b5726b93.png → 18042dda-0222-4333-9d79-3afc3dc75220.png} +0 -0
  71. /kotonebot/kaa/sprites/{420e4ff4-82a3-4ea3-98a7-63efe65003a6.png → 18670c18-07e3-4378-a8fc-4f9b331db00f.png} +0 -0
  72. /kotonebot/kaa/sprites/{027dd54f-09d8-452d-8eff-66b06aecd264.png → 1959c932-1bbf-4fee-b3a1-2ecfbc6d4209.png} +0 -0
  73. /kotonebot/kaa/sprites/{b673cc06-14ef-4a22-b484-13a17b01b0d4.png → 1a8ec83b-ebba-4ff7-9737-104d3aa2ff98.png} +0 -0
  74. /kotonebot/kaa/sprites/{1e6d734e-7bc5-4f3f-a717-3931f44a7709.png → 1b6bc9ae-3f4e-4722-ae6f-d76bd65b071e.png} +0 -0
  75. /kotonebot/kaa/sprites/{0bd326fd-fad0-4e6c-957c-da08583ef231.png → 1e4671aa-2cdc-4432-9d83-c635e0a3d09d.png} +0 -0
  76. /kotonebot/kaa/sprites/{e941ae91-59bf-4a69-945b-1bcd7cf241c6.png → 2490bd58-bcf7-4c94-ab24-04e752137939.png} +0 -0
  77. /kotonebot/kaa/sprites/{f790aed5-b9f3-4c93-a14f-47629f53052d.png → 25cfb896-fed1-4361-ae64-fc45fc8a98dd.png} +0 -0
  78. /kotonebot/kaa/sprites/{6e81e54e-b69b-434a-8bbf-6e2551bddeb8.png → 2730beb1-a6c5-463d-9748-393764bc8e34.png} +0 -0
  79. /kotonebot/kaa/sprites/{3f0b91e1-898a-4f44-b116-38483bc30461.png → 2766af5c-4ad1-44e2-ab84-e8a6ac7bb172.png} +0 -0
  80. /kotonebot/kaa/sprites/{4fca090a-b94a-433a-8438-832a54ff65e8.png → 2cc68858-7af0-4a12-8c78-c4e4dcf9fff1.png} +0 -0
  81. /kotonebot/kaa/sprites/{04b4ccde-a374-4df3-b0fa-552ae1518348.png → 2e68066d-9a56-48cc-8711-1d4c080ee320.png} +0 -0
  82. /kotonebot/kaa/sprites/{4a9e612b-9e52-4eb5-a851-3e5ad68030e9.png → 32d50687-b3a2-4b34-a2f9-4418b5e14fff.png} +0 -0
  83. /kotonebot/kaa/sprites/{2179ff5c-7964-42b1-9a95-c0bd8af1b692.png → 344bedf1-fcca-4025-9ff6-cea99c91ddb4.png} +0 -0
  84. /kotonebot/kaa/sprites/{99e95c59-d5da-448d-b839-51428b2686f0.png → 346da82f-bfca-4a0f-bae4-ca9c8e4b794e.png} +0 -0
  85. /kotonebot/kaa/sprites/{be7504d5-48e5-404c-a74d-f7fa86acf276.png → 379a8811-932c-4c75-854f-943631c921d5.png} +0 -0
  86. /kotonebot/kaa/sprites/{d11d8839-90ef-433c-8555-fad5478009a0.png → 38f8f55f-e9dc-4c10-b97a-b7254d871ae9.png} +0 -0
  87. /kotonebot/kaa/sprites/{03c9182c-b5ba-4404-b6f9-50b06a86e102.png → 39b1738f-af9a-4c85-9315-2858f0b1d1ab.png} +0 -0
  88. /kotonebot/kaa/sprites/{fb529219-fc53-43e1-83ca-2313d1d33636.png → 3a58e019-6328-412a-9d2f-85a73900371e.png} +0 -0
  89. /kotonebot/kaa/sprites/{d4673e38-cf76-4062-9a71-0f31936fe49b.png → 3c2f70b5-9095-48d6-a012-4d68a6b0a975.png} +0 -0
  90. /kotonebot/kaa/sprites/{e753b92e-99bf-4d81-9673-92b5f67d129b.png → 3d016208-f8ed-45be-bfb1-ed15b231d0b8.png} +0 -0
  91. /kotonebot/kaa/sprites/{0b0e76ce-60cc-45cc-b204-227e59d39c26.png → 3e2af23e-041f-4128-8a78-cbd1297538cf.png} +0 -0
  92. /kotonebot/kaa/sprites/{edb144e1-7c82-4fcf-b426-c6294ad2112d.png → 429c30b5-7d3a-40a1-853a-ec51c8f238e7.png} +0 -0
  93. /kotonebot/kaa/sprites/{de619141-10a4-4447-89b3-6cabcbf52d8a.png → 440eeea3-b710-485e-ab40-80e2b6ce66f0.png} +0 -0
  94. /kotonebot/kaa/sprites/{1607c23d-3ab4-418b-8114-1a3bb782d246.png → 46f24175-8657-497d-97e6-1c1ee356e4cd.png} +0 -0
  95. /kotonebot/kaa/sprites/{3eb037db-4cd8-4c00-a655-fa73f612adfd.png → 4a128979-bf11-42fe-a966-a8f5ee143128.png} +0 -0
  96. /kotonebot/kaa/sprites/{7b07a31c-9e2b-4ce0-8998-ccc0592de5e0.png → 4a5ec641-38ec-46b0-83c4-d7edacb754b0.png} +0 -0
  97. /kotonebot/kaa/sprites/{e97e58cb-3412-4a8c-9a9e-dd45daa5ae02.png → 4a9c1ac7-0a61-4d40-b749-e8896322581f.png} +0 -0
  98. /kotonebot/kaa/sprites/{9fd268df-96b7-4b48-a6d1-b5da2161da8e.png → 4af7631b-3674-4491-a3fb-a514bfd6b204.png} +0 -0
  99. /kotonebot/kaa/sprites/{32a1072b-c359-4dad-80f6-c9ca558e5f99.png → 4c2c5378-d946-41c2-980e-afb8493f71ee.png} +0 -0
  100. /kotonebot/kaa/sprites/{1a668ad4-d6b0-4aed-bb99-8b1ac85659d3.png → 4c4dd8e5-5370-4381-b6d4-c260b32015cf.png} +0 -0
  101. /kotonebot/kaa/sprites/{32793e3b-613b-4b6d-bc2e-25f9993f6274.png → 4c5b8c40-3dfa-4091-95c3-257a36e9a6d3.png} +0 -0
  102. /kotonebot/kaa/sprites/{d3fa5988-1184-4478-abb9-1c9799bd4100.png → 4d0b8533-b3e2-4ea1-8204-a8d5e67e1f64.png} +0 -0
  103. /kotonebot/kaa/sprites/{0de838fb-24e9-4fe8-8e2a-cc26bce6d7d8.png → 50e462d5-ca14-4934-b35a-da3ebb8fb33f.png} +0 -0
  104. /kotonebot/kaa/sprites/{d2830bf7-e0c4-4d00-868a-6df22bdf6fcd.png → 557389eb-d0da-4c5a-a322-de3e32a81909.png} +0 -0
  105. /kotonebot/kaa/sprites/{979c7468-d9ef-4cc1-9449-2aa716caabb0.png → 57b7006a-ada9-4d9d-9655-71765251882e.png} +0 -0
  106. /kotonebot/kaa/sprites/{8f39322e-92d2-4cc6-80ef-341884b0a598.png → 59ef821b-4803-4fe3-83b8-305902f8459a.png} +0 -0
  107. /kotonebot/kaa/sprites/{a0b83694-4945-4476-aa2e-f268841ac372.png → 5af71b93-310c-48ab-b438-dba296fb9e52.png} +0 -0
  108. /kotonebot/kaa/sprites/{a03e79b3-7f44-4211-b625-2510ba8031bc.png → 5b6b5bf8-cd03-4360-9550-e15d8db6241a.png} +0 -0
  109. /kotonebot/kaa/sprites/{2fd33c51-b3a7-4ffb-9555-d25961186eee.png → 5d963589-623d-4a49-9c72-af91134720b0.png} +0 -0
  110. /kotonebot/kaa/sprites/{f4c8a757-674e-45ca-af2c-0bc409b7bca8.png → 5f01b811-9aad-4439-a70b-c099a42c36fa.png} +0 -0
  111. /kotonebot/kaa/sprites/{a1c29e77-138c-4580-a777-666815039524.png → 601d0743-7143-4c04-989b-b80fc620e678.png} +0 -0
  112. /kotonebot/kaa/sprites/{4aed7e90-76b1-4837-a6ad-e2ed2e3240ec.png → 63e71ec7-6290-4efa-89cf-69c53d805a58.png} +0 -0
  113. /kotonebot/kaa/sprites/{c15ad104-3814-467c-a211-1e54be35fc66.png → 66f5fab8-274c-4ab9-983d-ca2f8d90ea94.png} +0 -0
  114. /kotonebot/kaa/sprites/{8031a52b-afe1-4ebd-9599-1d123bc0e48b.png → 6abb63c1-025a-425e-8f33-29f83f504d75.png} +0 -0
  115. /kotonebot/kaa/sprites/{ad26884a-8371-420f-be89-7fe82ddaf1e8.png → 6d6550a6-f270-474e-b55c-b2017ef87ab2.png} +0 -0
  116. /kotonebot/kaa/sprites/{bf158b17-4d93-40f1-b9cf-4ae9226bcfc1.png → 7057e165-9ed4-485e-9609-94854ef269dd.png} +0 -0
  117. /kotonebot/kaa/sprites/{fa64ed03-4277-4ae2-a958-adb93c393457.png → 78c2c39e-6621-41d1-9a1e-7e171b001795.png} +0 -0
  118. /kotonebot/kaa/sprites/{f32e04e8-2e9a-49c2-9734-d0a02ed2d892.png → 798a9587-3f8c-424e-90af-89a08ed62ced.png} +0 -0
  119. /kotonebot/kaa/sprites/{b677c2ba-62da-4afd-8ba7-a9b40362b5bb.png → 7bbac9cd-417f-4bd5-975a-a8a25c7bd922.png} +0 -0
  120. /kotonebot/kaa/sprites/{d1a8f72b-813b-4d8a-a596-5576758d7fe2.png → 7c2f9216-56f2-4eec-8a45-1bd864316dad.png} +0 -0
  121. /kotonebot/kaa/sprites/{fe7e4f2b-b795-43b6-bf0a-4e65c9133a40.png → 7c992bae-5ded-473b-8e60-4dede8434e5d.png} +0 -0
  122. /kotonebot/kaa/sprites/{e73cacc9-08aa-4b7a-acd3-3dbe95d9f51c.png → 7e7e8772-b31a-4bbe-919e-90373a4ad5a7.png} +0 -0
  123. /kotonebot/kaa/sprites/{39151ffc-365e-45b2-a2f4-9d4c76d41524.png → 80171520-d132-4395-b2ed-e6451335b3d5.png} +0 -0
  124. /kotonebot/kaa/sprites/{04180b4a-1532-42dd-9280-0f47c1e68959.png → 82cf688e-ac85-4425-917d-10b6db9b6cc3.png} +0 -0
  125. /kotonebot/kaa/sprites/{ebcaaba7-57f8-4bcb-93da-a288a0fcc95e.png → 87292951-43c1-4039-9b92-c59c1b25b5f7.png} +0 -0
  126. /kotonebot/kaa/sprites/{4dd7db2d-3107-4fc5-8f30-cfa70bc28de1.png → 893a6bba-905e-4123-9e35-f829fd6c8b2a.png} +0 -0
  127. /kotonebot/kaa/sprites/{f9e0be54-237b-4092-8784-1a4d2eb43097.png → 8a605d31-3160-49d1-9098-45d8ee18e8d6.png} +0 -0
  128. /kotonebot/kaa/sprites/{334d4915-4600-4c81-bbb0-c52291b7d089.png → 8cb3b6c2-7fae-41cf-8f33-a0e3ea668f25.png} +0 -0
  129. /kotonebot/kaa/sprites/{6abe81b7-1a04-44e9-b390-affe47799c46.png → 8e1ca316-39e6-4b9f-b497-84ad8848a3e1.png} +0 -0
  130. /kotonebot/kaa/sprites/{f14cc536-f38c-4568-9812-d3ed95e639d3.png → 8f675963-122a-4932-9f33-07dcdcb3a51a.png} +0 -0
  131. /kotonebot/kaa/sprites/{0df088e0-737f-44ec-b39f-76239d087382.png → 907dc373-e2f5-4959-84cb-d7971a7fefd8.png} +0 -0
  132. /kotonebot/kaa/sprites/{7d5b4d44-cb46-49fb-96be-edaab5396836.png → 9359526e-c5db-4d63-adc4-08b7d704b422.png} +0 -0
  133. /kotonebot/kaa/sprites/{2860421f-2db8-4850-9f0c-0aca8bbf16d0.png → 96178cef-4868-4378-9f9c-c08780def3d6.png} +0 -0
  134. /kotonebot/kaa/sprites/{a477c873-98bd-45f0-a9a6-324e714c69cd.png → 985ec867-bc7c-47c5-8fcd-75f736f27507.png} +0 -0
  135. /kotonebot/kaa/sprites/{669f9e7c-b665-47a8-b4f5-fb2748bf88af.png → 9c98c4e6-5335-4c8b-92fa-24ec440b055e.png} +0 -0
  136. /kotonebot/kaa/sprites/{1373ec63-fd98-4be9-a925-08144f1d5ba5.png → a015cea0-d2c7-439a-b942-d4d2dad276e5.png} +0 -0
  137. /kotonebot/kaa/sprites/{6fec0efa-336f-4726-b1d9-dc83c23b297d.png → a05abc9d-9f0e-4d83-94bb-4f0b1e061035.png} +0 -0
  138. /kotonebot/kaa/sprites/{957dbdc4-8367-4a59-9bfc-d35ea11af9b8.png → a2e41d98-dd91-4e51-ba57-4f8ddfd07cfa.png} +0 -0
  139. /kotonebot/kaa/sprites/{8e78e1f1-ea2f-4779-a8cd-56336f936c81.png → a402b549-a4dd-4ff3-ad51-2aa5f8e620af.png} +0 -0
  140. /kotonebot/kaa/sprites/{ff493975-0f30-468f-a3ea-17505a2b0385.png → a5efb332-8452-49d8-ade3-882bbd445553.png} +0 -0
  141. /kotonebot/kaa/sprites/{dfe2fc89-eaf7-4027-bb53-dd17a9741e01.png → a9f47d53-fbef-4cb0-9d2d-845ef3d1f719.png} +0 -0
  142. /kotonebot/kaa/sprites/{45ae4d17-1887-4f2e-8cce-e5a4cc42e10b.png → b10596fe-22fa-4284-ab4b-1d653d4f0f07.png} +0 -0
  143. /kotonebot/kaa/sprites/{9cc7997f-3aeb-4b20-ae42-468ebfbc851e.png → b326c50f-c88c-4b99-80a7-046053423a19.png} +0 -0
  144. /kotonebot/kaa/sprites/{6d706642-441f-4bcc-b377-0fce3484be22.png → b44085c7-9777-415a-aae1-aae47c2f7321.png} +0 -0
  145. /kotonebot/kaa/sprites/{54d57991-e5d0-4346-b1f8-63e429d8a3fe.png → b629f0a7-df4f-4407-908b-a031c70e8b6f.png} +0 -0
  146. /kotonebot/kaa/sprites/{63f28640-766f-4698-b556-f0a0cbac6a1a.png → b8165178-a9ce-491a-ba57-d25c7af8533f.png} +0 -0
  147. /kotonebot/kaa/sprites/{0c3cc689-90f5-4c29-be80-04e2f2ab799a.png → b8eeacac-5679-42b9-a5b9-138598c8c6b6.png} +0 -0
  148. /kotonebot/kaa/sprites/{f89cafb7-657b-4ab4-8917-1964b242ff2e.png → b9ad5832-889a-4a5f-adc0-5dfd55126d6a.png} +0 -0
  149. /kotonebot/kaa/sprites/{16576157-f99b-4a64-b41b-3ee712c2304e.png → bcb95d32-32e1-4251-83de-ef5164d22a48.png} +0 -0
  150. /kotonebot/kaa/sprites/{7e64561b-0dab-420a-8f79-45ed6801939b.png → c0ddf15a-a6f6-42f6-86b7-e17cde60b437.png} +0 -0
  151. /kotonebot/kaa/sprites/{6d4f414d-ccd8-4a25-aa32-1b77ce0035d6.png → c24e874e-6a3e-472c-8745-edf7cac88954.png} +0 -0
  152. /kotonebot/kaa/sprites/{2b87ed7c-b01b-4ea7-ab34-ec4e37e036c0.png → c362b54d-d535-4227-8612-9bfe11fd7891.png} +0 -0
  153. /kotonebot/kaa/sprites/{1e7924dc-9dd4-4d54-add8-b394c75af1c7.png → c51132e9-f45c-40c9-8026-7d70b13dca8b.png} +0 -0
  154. /kotonebot/kaa/sprites/{5663551f-5abc-4ef8-a3fd-ba57b20cb35c.png → c60bec93-231f-498d-b3c6-0a78c9b8f37f.png} +0 -0
  155. /kotonebot/kaa/sprites/{89d6407a-982c-4dbc-8dd7-89cf79429eec.png → c64ff0a8-9f96-4ca7-902b-4f7298c1c431.png} +0 -0
  156. /kotonebot/kaa/sprites/{bda40c0d-55a9-41ad-ba8a-e8aafc52608b.png → c819d999-fef6-47bb-a94f-055d616033c7.png} +0 -0
  157. /kotonebot/kaa/sprites/{1b8b2a66-fb45-41d7-8441-04425a47bdeb.png → c85e58d3-ec5a-43ea-aa2a-04ee28e9ea21.png} +0 -0
  158. /kotonebot/kaa/sprites/{b332eb23-6653-4d17-b779-2451a3b261ab.png → c883049b-187c-4685-a63a-cff516851a95.png} +0 -0
  159. /kotonebot/kaa/sprites/{60f292d1-6c2d-4cfb-af76-568610817722.png → c8f0cc49-5df8-4d7a-9ef6-2f021c2380a5.png} +0 -0
  160. /kotonebot/kaa/sprites/{7383d7d0-8ee2-4413-b590-51cff541cab3.png → c9a8d73f-f487-45ff-aa05-a61393d27f66.png} +0 -0
  161. /kotonebot/kaa/sprites/{817c22a3-ecfe-4949-a784-59342e61a880.png → cb4c357f-b01b-4b95-b928-5affdf4ae684.png} +0 -0
  162. /kotonebot/kaa/sprites/{dff3509c-f0ec-44e2-8e0b-3c6b465d45f3.png → cc0e38b2-c2ae-4fe1-adb0-6ea73a602563.png} +0 -0
  163. /kotonebot/kaa/sprites/{c7a33e9e-2f1c-4171-b1f7-7d7dcc1a7bd3.png → cdd40676-b49d-42ad-9972-7149aa13d9bb.png} +0 -0
  164. /kotonebot/kaa/sprites/{5d912f4e-c319-4d45-85e6-de6e684a6278.png → cf76eb98-44ff-406e-913a-503a0233b4cd.png} +0 -0
  165. /kotonebot/kaa/sprites/{7b195535-7362-492f-8d87-b8eed15c6fc0.png → d21117c2-3679-4bb0-a5b3-5af5d12d8d3e.png} +0 -0
  166. /kotonebot/kaa/sprites/{7d778f82-f908-4edb-9b12-d8460589ab11.png → d37bc673-9668-4b7e-bc9d-54495129dcc0.png} +0 -0
  167. /kotonebot/kaa/sprites/{08750a02-0881-443a-9c69-c90a2ccf2a63.png → dac24ef9-e53d-4d95-aa09-fa694606c3ab.png} +0 -0
  168. /kotonebot/kaa/sprites/{31271771-5149-447c-8fae-ccc308c0a838.png → dd084696-be33-4a00-b6df-b25131b78b40.png} +0 -0
  169. /kotonebot/kaa/sprites/{a35aed53-ff78-41f0-9a19-c69fda40aeef.png → de1e3c4a-f6ed-467a-889c-25c01a613477.png} +0 -0
  170. /kotonebot/kaa/sprites/{639465a0-3c63-4307-a72c-4f9e543d2095.png → e1eb4596-73a1-471e-892b-c83cfb706bb3.png} +0 -0
  171. /kotonebot/kaa/sprites/{91940a9e-2cfc-423a-86d6-49429c051ea0.png → e1ec3ee5-e73c-44b6-b173-ab1c9382cba4.png} +0 -0
  172. /kotonebot/kaa/sprites/{096d3df5-2c95-41dd-bcc3-2e3afab47420.png → e4d076bd-7824-4496-a498-d6dc01ad90ba.png} +0 -0
  173. /kotonebot/kaa/sprites/{50a9e2a0-1166-4f1d-ad15-d8493ce0e49e.png → e551148d-1b0d-4518-8711-4161863c93ef.png} +0 -0
  174. /kotonebot/kaa/sprites/{45671af1-d731-4bb1-83f7-c9d23348308f.png → e61d855e-84d3-4a0e-b996-43fce262c2c3.png} +0 -0
  175. /kotonebot/kaa/sprites/{608217f6-b880-42ab-9be9-b4d56df87be2.png → e795f9c5-fdda-4fbc-8d38-a92512a590f4.png} +0 -0
  176. /kotonebot/kaa/sprites/{7d4d708f-c629-4f52-a0af-11aee7fcb878.png → eb446c0a-1dd2-4693-838e-98a76c02eac5.png} +0 -0
  177. /kotonebot/kaa/sprites/{35e6ad3a-8020-4619-b8ec-6506a8aa51fc.png → eb4bde42-de6e-4c62-8dc4-8885efc834a8.png} +0 -0
  178. /kotonebot/kaa/sprites/{9a5e7bc9-830f-4b01-8f94-6d4f589754c0.png → ec547e11-4952-4089-9744-1e89bd44389c.png} +0 -0
  179. /kotonebot/kaa/sprites/{40e3dac1-74fb-4501-a20f-18422d070554.png → ed4a8cff-b3df-4d7a-bfc4-8da776a8e548.png} +0 -0
  180. /kotonebot/kaa/sprites/{39d57097-6839-4030-bb51-24d6cd880897.png → efc08f1c-e78f-4df8-bff9-b45cde37f9dd.png} +0 -0
  181. /kotonebot/kaa/sprites/{9f56889b-0251-4ec1-8ccb-5cfd3bddd396.png → f02d997a-a3a8-4e18-96f1-e4176c14a5e0.png} +0 -0
  182. /kotonebot/kaa/sprites/{97bbc57d-780a-40f4-8fe1-bcaef697b601.png → f5e2e4e1-40be-481d-9293-9d47a5bce7cc.png} +0 -0
  183. /kotonebot/kaa/sprites/{bee4fbe2-365f-4145-b8f4-87c2c460cd71.png → f62dd2a2-bac6-4dc8-9a19-3e995ab80a3b.png} +0 -0
  184. /kotonebot/kaa/sprites/{666b1e8c-19fb-49e9-a761-1069f635f450.png → f64cfd95-8c87-47e6-96c3-8b7df2624ae3.png} +0 -0
  185. /kotonebot/kaa/sprites/{847cb208-a71c-42e3-97f1-18d4aa886b41.png → f8bcbc2c-9831-4e8d-9ffd-e532aa3193c3.png} +0 -0
  186. /kotonebot/kaa/sprites/{9e5c5ffc-450d-4ddc-a4ae-27c33267b056.png → fb50c7d1-b983-4e36-a998-ea17a1bdb18b.png} +0 -0
  187. /kotonebot/kaa/sprites/{f1a60528-1f35-451f-92e3-b1d5407e0f4e.png → fb97d804-42e4-45e2-8b22-01d16b5d0b6b.png} +0 -0
  188. /kotonebot/kaa/sprites/{a3140a53-ffb4-4094-a39c-240e9fe42308.png → fe337541-a6a3-4325-97b4-2bf27f51f39f.png} +0 -0
  189. /kotonebot/kaa/sprites/{1a0f2a10-c23b-47c7-aa7e-89b606f4d8ae.png → ffd3c6b7-cac2-4d45-ae37-792404ea2b40.png} +0 -0
  190. {ksaa-2025.5.16.1.dist-info → ksaa-2025.5.23.1.dist-info}/entry_points.txt +0 -0
  191. {ksaa-2025.5.16.1.dist-info → ksaa-2025.5.23.1.dist-info}/top_level.txt +0 -0
@@ -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.util import Rect, Point, is_rect, is_point
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, HintBox):
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[0] + rect[2] // 2 + np.random.randint(-int(rect[2] * 0.3), int(rect[2] * 0.3))
171
- y = rect[1] + rect[3] // 2 + np.random.randint(-int(rect[3] * 0.3), int(rect[3] * 0.3))
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 is_rect(arg0) or isinstance(arg0, ClickableObjectProtocol):
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)
@@ -1,4 +1,4 @@
1
- from enum import Enum
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
- result = adb.connect(addr)
21
- if 'cannot connect to' in result:
22
- raise ValueError(result)
23
- d = [d for d in adb.device_list() if d.serial == addr]
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__ = ['HostProtocol', 'Instance', 'CustomInstance', 'create_custom']
6
+ __all__ = [
7
+ 'HostProtocol', 'Instance',
8
+ 'CustomInstance', 'create_custom',
9
+ 'Mumu12Host', 'Mumu12Instance',
10
+ 'LeidianHost', 'LeidianInstance'
11
+ ]
@@ -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, adb_emulator_name: str, emulator_args: str = "") -> CustomInstance:
67
- return CustomInstance(exe_path, emulator_args=emulator_args, id='custom', name='Custom', adb_ip=adb_ip, adb_port=adb_port, adb_emulator_name=adb_emulator_name)
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)