-
-
Notifications
You must be signed in to change notification settings - Fork 77
Added a BlueStacks instance selector in the GUI with Refresh, persist… #616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
77eb439
b5dab42
5f923c8
39236bb
c45e60e
88b4c1b
8103d34
9a3c306
e5fd1e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,4 +13,5 @@ recordings | |
| .claude/ | ||
| .claude/settings.local.json | ||
| .DS_Store | ||
| *.egg-info | ||
| *.egg-info | ||
| .vscode/settings.json | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,46 @@ | |||||||||||||||||||||||||
| DEBUG = False | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def list_bluestacks_instances() -> list[str]: | ||||||||||||||||||||||||||
| """Return display names of available BlueStacks instances without side effects. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Reads registry and bluestacks.conf directly to avoid launching/stopping instances. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| if is_macos(): | ||||||||||||||||||||||||||
| base = "/Users/Shared/Library/Application Support/BlueStacks" | ||||||||||||||||||||||||||
| bs_conf = os.path.join(base, "bluestacks.conf") | ||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||
| import winreg | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\BlueStacks_nxt") as key: | ||||||||||||||||||||||||||
| data_dir, _ = winreg.QueryValueEx(key, "DataDir") | ||||||||||||||||||||||||||
| except (OSError, FileNotFoundError): | ||||||||||||||||||||||||||
| # BlueStacks not installed or registry key not accessible | ||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||
| dd = normpath(str(data_dir)) | ||||||||||||||||||||||||||
| bs_conf = os.path.join(os.path.dirname(dd), "bluestacks.conf") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if not os.path.isfile(bs_conf): | ||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||
| with open(bs_conf, "r", encoding="utf-8", errors="ignore") as f: | ||||||||||||||||||||||||||
| text = f.read() | ||||||||||||||||||||||||||
| names: list[str] = [] | ||||||||||||||||||||||||||
| for m in re.finditer(r'^bst\.instance\.([^.=]+)\.display_name="([^"]+)"\s*$', text, flags=re.M): | ||||||||||||||||||||||||||
| names.append(m.group(2)) | ||||||||||||||||||||||||||
| # Deduplicate preserving order | ||||||||||||||||||||||||||
| seen = set() | ||||||||||||||||||||||||||
| ordered = [] | ||||||||||||||||||||||||||
| for n in names: | ||||||||||||||||||||||||||
| if n not in seen: | ||||||||||||||||||||||||||
| seen.add(n) | ||||||||||||||||||||||||||
| ordered.append(n) | ||||||||||||||||||||||||||
| return ordered | ||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| class BlueStacksEmulatorController(AdbBasedController): | ||||||||||||||||||||||||||
| """BlueStacks 5 controller using HD-Adb. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -26,11 +66,11 @@ class BlueStacksEmulatorController(AdbBasedController): | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| supported_platforms = [Platform.WINDOWS, Platform.MACOS] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def __init__(self, logger, render_settings: dict | None = None): | ||||||||||||||||||||||||||
| def __init__(self, logger, render_settings: dict | None = None, instance_name: str | None = None): | ||||||||||||||||||||||||||
| self.logger = logger | ||||||||||||||||||||||||||
| self.expected_dims = (419, 633) # Bypassing bs5's stupid dim limits | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| self.instance_name = "pyclashbot-96" | ||||||||||||||||||||||||||
| self.instance_name = instance_name or "pyclashbot-96" | ||||||||||||||||||||||||||
| self.internal_name: str | None = None | ||||||||||||||||||||||||||
| self.instance_port: int | None = None | ||||||||||||||||||||||||||
| self.device_serial: str | None = None # "127.0.0.1:<port>" | ||||||||||||||||||||||||||
|
|
@@ -345,6 +385,35 @@ def _pick_unlinked_instance(self) -> str | None: | |||||||||||||||||||||||||
| return internal | ||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def get_available_instances(self) -> list[str]: | ||||||||||||||||||||||||||
| """Get list of all available BlueStacks instances with their display names. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Returns a list of display names of all BlueStacks instances. | ||||||||||||||||||||||||||
| If no instances are found, returns an empty list. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| conf = self._read_text(self.bs_conf_path) | ||||||||||||||||||||||||||
| if not conf: | ||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| instance_list = self._list_instance_internals(conf) | ||||||||||||||||||||||||||
| if not instance_list: | ||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Get display names for each internal instance | ||||||||||||||||||||||||||
| display_names = [] | ||||||||||||||||||||||||||
| for internal in instance_list: | ||||||||||||||||||||||||||
| display_name = self._get_conf_value(conf, f"bst.instance.{internal}.display_name") | ||||||||||||||||||||||||||
| if display_name: | ||||||||||||||||||||||||||
| display_names.append(display_name) | ||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||
| # Fallback to internal name if no display name is set | ||||||||||||||||||||||||||
| display_names.append(internal) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return display_names | ||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def _ensure_managed_instance(self): | ||||||||||||||||||||||||||
| # If display entry exists | ||||||||||||||||||||||||||
| if self._display_name_exists(self.bs_conf_path, self.instance_name): | ||||||||||||||||||||||||||
|
|
@@ -534,6 +603,42 @@ def _refresh_instance_port(self): | |||||||||||||||||||||||||
| if DEBUG: | ||||||||||||||||||||||||||
| print(f"[Bluestacks 5] Refreshed instance port -> {self.device_serial}") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def _ensure_target_instance(self) -> bool: | ||||||||||||||||||||||||||
| """Resolve internal name/port for the selected display name; fallback to first instance.""" | ||||||||||||||||||||||||||
| # Already resolved | ||||||||||||||||||||||||||
| if self.internal_name and self.instance_port and self.device_serial: | ||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| conf_text = self._read_text(self.bs_conf_path) or "" | ||||||||||||||||||||||||||
| internal: str | None = None | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # 1) Try matching display name from MimMetaData or conf | ||||||||||||||||||||||||||
| if self.instance_name: | ||||||||||||||||||||||||||
| internal = self._find_internal_by_display_name(self.mim_meta_path, self.instance_name) or \ | ||||||||||||||||||||||||||
| self._find_internal_in_conf_by_display(self.bs_conf_path, self.instance_name) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # 2) Fallback: pick the first instance present | ||||||||||||||||||||||||||
| if not internal: | ||||||||||||||||||||||||||
| internals = self._list_instance_internals(conf_text) | ||||||||||||||||||||||||||
| if internals: | ||||||||||||||||||||||||||
| internal = internals[0] | ||||||||||||||||||||||||||
| fallback_display = self._get_conf_value(conf_text, f"bst.instance.{internal}.display_name") or internal | ||||||||||||||||||||||||||
| self.logger.log( | ||||||||||||||||||||||||||
| f"[BlueStacks 5] Requested instance '{self.instance_name}' not found. " | ||||||||||||||||||||||||||
| f"Using '{fallback_display}' instead." | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| # Update outward-facing name so window-title checks align | ||||||||||||||||||||||||||
| self.instance_name = fallback_display | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if not internal: | ||||||||||||||||||||||||||
| self.logger.change_status("No BlueStacks instance found in configuration.") | ||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| self.internal_name = internal | ||||||||||||||||||||||||||
| self.instance_port = self._read_instance_adb_port(self.bs_conf_path, internal) | ||||||||||||||||||||||||||
| self.device_serial = f"127.0.0.1:{self.instance_port}" if self.instance_port else None | ||||||||||||||||||||||||||
| return self.instance_port is not None | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def _connect(self) -> bool: | ||||||||||||||||||||||||||
| """Connect only to this instance's port.""" | ||||||||||||||||||||||||||
| if not self.device_serial: | ||||||||||||||||||||||||||
|
|
@@ -570,7 +675,7 @@ def _is_this_instance_running(self) -> bool: | |||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Windows: tasklist CSV parsing | ||||||||||||||||||||||||||
| title = self.instance_name | ||||||||||||||||||||||||||
| title = (self.instance_name or "").strip().lower() | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| res = subprocess.run( | ||||||||||||||||||||||||||
| 'tasklist /v /fi "IMAGENAME eq HD-Player.exe" /fo csv', | ||||||||||||||||||||||||||
|
|
@@ -583,13 +688,23 @@ def _is_this_instance_running(self) -> bool: | |||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||
| reader = csv.reader(io.StringIO(res.stdout)) | ||||||||||||||||||||||||||
| next(reader, None) # skip header | ||||||||||||||||||||||||||
| any_player = False | ||||||||||||||||||||||||||
| for row in reader: | ||||||||||||||||||||||||||
| if not row: | ||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||
| image = (row[0] if len(row) > 0 else "").strip().lower() | ||||||||||||||||||||||||||
| window_title = (row[-1] if len(row) > 0 else "").strip() | ||||||||||||||||||||||||||
| if image == "hd-player.exe" and window_title == title: | ||||||||||||||||||||||||||
| return True # yes, I parsed CSV from tasklist. no, I'm not proud of it | ||||||||||||||||||||||||||
| window_title_lower = window_title.lower() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if image == "hd-player.exe": | ||||||||||||||||||||||||||
| any_player = True | ||||||||||||||||||||||||||
| # Accept exact, contains, or empty title (fallback to any player) | ||||||||||||||||||||||||||
| if not title or window_title_lower == title or title in window_title_lower: | ||||||||||||||||||||||||||
| return True # yes, I parsed CSV from tasklist. no, I'm not proud of it | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Fallback: if any BlueStacks player is running, assume ours to avoid hanging | ||||||||||||||||||||||||||
| if any_player: | ||||||||||||||||||||||||||
|
Comment on lines
+705
to
+706
|
||||||||||||||||||||||||||
| # Fallback: if any BlueStacks player is running, assume ours to avoid hanging | |
| if any_player: | |
| # Fallback: if any BlueStacks player is running, assume ours only when no specific title is requested | |
| if any_player and not title: |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The soft_wait check will always break the loop after 15 seconds, making the boot_timeout check at line 768 unreachable. The condition order should be reversed so that boot_timeout is checked first to ensure proper timeout behavior. The logic should fail after boot_timeout but proceed optimistically after soft_wait if the instance hasn't been detected yet.
| if elapsed > soft_wait: | |
| self.logger.log("[BlueStacks 5] Proceeding without window title match; assuming launched.") | |
| break | |
| if elapsed > boot_timeout: | |
| self.logger.change_status("Timeout waiting for pyclashbot instance to start - retrying...") | |
| return False | |
| if elapsed > boot_timeout: | |
| self.logger.change_status("Timeout waiting for pyclashbot instance to start - retrying...") | |
| return False | |
| if elapsed > soft_wait: | |
| self.logger.log("[BlueStacks 5] Proceeding without window title match; assuming launched.") | |
| break |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra blank lines. There should only be one blank line between method definitions and two blank lines before top-level function definitions according to PEP 8 style guidelines.