From 6a4b56682c0a57b8a1f68b3d994590576a6e292d Mon Sep 17 00:00:00 2001 From: z060142 Date: Fri, 18 Apr 2025 13:17:48 +0800 Subject: [PATCH] Modularize existing work logic --- main.py | 26 +- templates/In_private_room.png | Bin 0 -> 4079 bytes templates/In_world_room.png | Bin 0 -> 4045 bytes templates/Previous_page.png | Bin 0 -> 4685 bytes templates/Private_Label_normal.png | Bin 0 -> 4545 bytes templates/World_Label_normal.png | Bin 0 -> 3332 bytes templates/World_map.png | Bin 0 -> 15607 bytes templates/chat_room.png | Bin 5821 -> 4713 bytes ui_interaction.py | 959 ++++++++++++++++------------- 9 files changed, 543 insertions(+), 442 deletions(-) create mode 100644 templates/In_private_room.png create mode 100644 templates/In_world_room.png create mode 100644 templates/Previous_page.png create mode 100644 templates/Private_Label_normal.png create mode 100644 templates/World_Label_normal.png create mode 100644 templates/World_map.png diff --git a/main.py b/main.py index c39c1bf..b7cbc1d 100644 --- a/main.py +++ b/main.py @@ -24,8 +24,9 @@ all_discovered_mcp_tools: list[dict] = [] exit_stack = AsyncExitStack() # Stores loaded persona data (as a string for easy injection into prompt) wolfhart_persona_details: str | None = None -# --- Use standard thread-safe queue --- -trigger_queue: ThreadSafeQueue = ThreadSafeQueue() # Use standard Queue +# --- Use standard thread-safe queues --- +trigger_queue: ThreadSafeQueue = ThreadSafeQueue() # UI Thread -> Main Loop +command_queue: ThreadSafeQueue = ThreadSafeQueue() # Main Loop -> UI Thread # --- End Change --- ui_monitor_task: asyncio.Task | None = None # To track the UI monitor task @@ -205,8 +206,9 @@ async def run_main_with_exit_stack(): # 3. Start UI Monitoring in a separate thread print("\n--- Starting UI monitoring thread ---") loop = asyncio.get_running_loop() # Get loop for run_in_executor + # Use the new monitoring loop function, passing both queues monitor_task = loop.create_task( - asyncio.to_thread(ui_interaction.monitor_chat_for_trigger, trigger_queue), + asyncio.to_thread(ui_interaction.run_ui_monitoring_loop, trigger_queue, command_queue), # Pass command_queue name="ui_monitor" ) ui_monitor_task = monitor_task # Store task reference for shutdown @@ -265,15 +267,16 @@ async def run_main_with_exit_stack(): if thoughts: print(f"AI Thoughts: {thoughts[:150]}..." if len(thoughts) > 150 else f"AI Thoughts: {thoughts}") - # 只有當有效回應時才發送到遊戲 + # 只有當有效回應時才發送到遊戲 (via command queue) if bot_dialogue and valid_response: - print("Preparing to send dialogue response via UI...") - send_success = await asyncio.to_thread( - ui_interaction.paste_and_send_reply, - bot_dialogue - ) - if send_success: print("Response sent successfully.") - else: print("Error: Failed to send response via UI.") + print("Sending 'send_reply' command to UI thread...") + command_to_send = {'action': 'send_reply', 'text': bot_dialogue} + try: + # Put command into the queue for the UI thread to handle + await loop.run_in_executor(None, command_queue.put, command_to_send) + print("Command placed in queue.") + except Exception as q_err: + print(f"Error putting command in queue: {q_err}") else: print("Not sending response: Invalid or empty dialogue content.") @@ -309,4 +312,3 @@ if __name__ == "__main__": print(f"Top-level error during asyncio.run execution: {e}") finally: print("Program exited.") - diff --git a/templates/In_private_room.png b/templates/In_private_room.png new file mode 100644 index 0000000000000000000000000000000000000000..8cd439066a9608c622a177b5ad0de67298ef7749 GIT binary patch literal 4079 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

Nt|Bqk=>+uQS^2!AduEzNsR08=VCIoaCUnis{F zFUvf=*2Z7C%HnMA85kJIbA<2RDlFNzZ{HRd7jNCV_1w90%=cOwxZzJ2h(Q6_XmDTv zGC~F&!T$^aFFJ8N zjJW@Z1RP)I9}q}1Sz1~GiJY7qq#q6T10gQi?%ub5&pxJ0nj5^mX3cQm{>;nE-{7;E zbWICzVy@ur)z#IQLr8}zDk@0B4h}r3!ebwjG=SoZ7cXZzIp;jerBj&W>^OVo3`Gii zCdtFh%oVHFWM*gcZJj;CVUDwtqT*Mb#Kv8kGtZr_!^Iv89Hvbr?z~`${0WY;cxi*C z_gt~ee#&HS##UBV7I!&*jrV%K%1CxkpZSAtQ-3|ZZCf_=R#in0hx`MM1_jf-k#wW_ z^XJcng+-XqJ9qD42`uwm=Q!BglkCGQ4gbwlOx&fEl+?eftEo-eeYH%Djg^&qG&l}$ zki7i-+jo)+3JXb1eYG@AIXg@w$5LAQqWH-ZU|B=C5(k&4rJ-_3Bko@e_ON>Vk?GJy zQAt)#R#8C#>64O@qM?3&<~SR=HMQ^Fy(Lx3$jC}a&~{Q$ z@#@nj@&+T{!&?p{v>uq@DHi+KuL`cLwysW0Ossvob}$??E2PQ~`d6sGL@4=%;?E$z hQ2ZI>7m7cF{1?rRx*+T*E$RRO002ovPDHLkV1ne#=Y;?O literal 0 HcmV?d00001 diff --git a/templates/In_world_room.png b/templates/In_world_room.png new file mode 100644 index 0000000000000000000000000000000000000000..579dfedc23f346f025d8427850cc808d23e2ce86 GIT binary patch literal 4045 zcma)MyUBLxKoNLx$&3HeMVFEs`# z@~#ANs-~b|N!3u7E4;Wy1>7!CNST)~eG z95K(F{2rldz1L7oI^y_|Iw2iE$Cl@prXy}6syH=(c5)IF6S^dB-)YcQLn2i|a?jXY z2StQSzo4r=JkX4To0w}~;%8HCY1 z%Obzs#~dGnhE{Ge0GYSu&@QfhLqq2sZ9PZn7?dg{bV%ll>5<>2P+98LS^Zr10EB=7 zLv=wuGH>m^IsCZhu>BMfvK^D+r>2&fnR!Yg!7X!@f<8X72}}N@HhN4!#X>nvo&9S* zRDg|*&BV;u_$Of?WpZ-Lve`SSe$Sj&Ton3vH=#W#DGA@Yv8%6J+t~OT%%NnZrYO%( zEI##AR#rALGAgt8^E+wr*=%s1+1g$=GB$otRIs$ZE+i@nb;Un=R?Dp%OjupbOi#~J z-fY%*`BQl%!r6Ql9~l*SJyD?D-}AXSLn!@K$Y#Y37Y(62HKqN!cliI^zCU0b9v&qm zBorGPi(g0+w-U2x7#`?GA&kblXZVC5h57l^Pw!QT$jH=_NTj8(d3vL^uVMC%Swga4Dmhuhv41S& zAP@+GAwlDSN6HOf=Ck^p6Ms_OZfa_(rl!_1s{kTic>bx{Pc1Q)l9XI)_VV>r!W=yJ zvQ_hcPT=GI9UIF?5}SI+!()B*_p-<`%e&66=6gTI{w{B2NeSYa=J%c+Dqo1Jcv)?= z-M5|(HGT;lNdrbO7z`Vo^X2?x7wzn9d2-Uu<}D%v&9ATbxghPEOFek-fSWt_+$K-= zmbh(pJmu1FZ7vn90_A8`*`AZ30RX}6_h)-I$Je_H?b~yWKMzyhzpuCb%BVFP)z80I z-Pp+fevcw3Ci`p?ULpb5KRc23T&$H-TiyImvDl4Du%j=eC&&;>B_V$sHktB40Gmz3GbbfyR!ni27tg^A8 zK`H1=dWy~js5Ls0Df#nxIfQqr)8UO}W9z(+gv&7~)pdfKdUHgYXvj%*>3*@;w-e z_~mXn=j_&mFEmO!ejt5ay3Q?s+PgFl+;#A*wb zkUKk5R>ESVzOK&6#pQERkwBkP7+b_w)jarfdwU}XTtndB3yiU`aRKeYtz(slbVV}( z7-&S4T?`o_c|$HLA|iq;9NRhi6vafqr{pK-sj|>NRqYxY8dZ{idU|_%$H$ZLrbuK~ zON%_R>s=8xXx; z9Y)KP`1bU0H9sJ!huS5Ol;`*(X8?x-N*mx?zh^6~uJ8EH&wk*-2tLxziHIO&%Du4J z4dryg8i6D+p>p8{kk${Wsl(X{?30l=)_1u@qF9+XmtAWr#3fR*4h=1>QTrb&6B83S z9NW-P(b96eU|nSy$jm%7Ia#mdzPPh~j2_5@qNmOeytMW#8r?aaOv3fJ?o(d7e%-7R z6~7(HN2Ng}#%(^F1-U*BEa2O^AACTFDJ?CFo5B z^B)h2IkcUfv8k!gazAJ`S}3H*Ft@b07#p{}wPhU_7!*_;+;x@rI9f%i%Y`nCO%%q2 z*UfvTQsD`*X71J-pKrP+e8wh$;fecA7WHB;JzSn_V0Qi(1$hMkE>jWn#Ln(|MS6j| zz+8hH6H_n7L=s1+ZwP)?=Ng%pmzR*h(r_|WZc$!dE(oGqrh1=MUM~8EVU~!+^01yf zefrc~`PIn|2nbZK@MQ5`T3()(eXZ z761U4X}*5xLDA2kK=WqQs>WmS%RCb7U0|)UIF=8)Bj=u)oH{Hb*u}>pm_}wzVw&L) z4h!Qtn4l8VO8^5#(g9@@HjUxCO8#Jd6)u(^YZVngn&@x|%+3e*ca$F~xN8IkXfvVF z=;8}(G1-+I`nj~t<{DihHPporH4((&7{-XU)1{@z7r&Rp!rI!}BAM8QD^d;hi*y&> zaVqdgHA56iOUpjW`v{NY_N+gqMHkgFy+9t=$*2u$o1(7{?dp$$k_bUp3lHuqaeoF2x<>U^T6KQzSfXHr>*&NTnpro7 z=7rLF4i4~T9{^9J>5tkA3s=Z4x6T=V-)BJ+bi9^sjyzyR^hpJH-{OlW`hcPcfW5=% zpbM?t$@r+KsKG&<;Nal02?F^ol*Aon60Z=4yTg2wXWxhaWvJW>RAE_aY8KV2UuVag zG240LAx4>e507{*79y3!tvWgSW&uoF8?C+MY>5|wEo;}Os9IU|{{@Xp0&uqX}ca^$}jj5}zPx^ok zHfw2ZWy!o7Y0FIDE+GY zMB7h?6=!EWFXDXsUhC=@C^>mYOkuAIm*0!91+ip>cI5{}({>wOx695}u#uLLZuDNf zWTOg=YWbMfsvwGysP>$YGxpeT&Dklr?hA;CiH^Hc>LCNu)!A8FS9jT(>KsJGBP>At zn8`^QE|ep;Qml;==447AGV`~nM5!Z~3$*~;E>FnZblNT0snFLSvEN;=BpqJkJA|ZE z&pbn@CGiO2t<m1^qF8K3I6T+65_FZ;GHXmSRHQA&kDy zp(kA4o{GAhPNbjHQeDjKk$b_D!j?`6lKO1;_yDHu?EHOme!QmU-8%!MU4>0AzrjaA zfd}DdEH}$KH$HwO6P;yJ<`0b`HrRxQ*fdkc&GdFSNA%%CMMt8mQb*^;&g^WyJua7S$JqFN|Ma52ZXFW~Gd?hQa9H&|cvFfxzOx z5St!{@ujy$>Snds?VRUkGy%?K@-Mzzje0&Ui z5uJY(AOAP{b#7bl4xvic>bL`R@nY|2Owuum?_i`Cf}% S_)Q+uDYP{V)GO4i!~O>!8qnnc literal 0 HcmV?d00001 diff --git a/templates/Previous_page.png b/templates/Previous_page.png new file mode 100644 index 0000000000000000000000000000000000000000..879f555bf093cc27ded72da934fb396479ade034 GIT binary patch literal 4685 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 P00000NkvXXu0mjf5q|5y literal 0 HcmV?d00001 diff --git a/templates/Private_Label_normal.png b/templates/Private_Label_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..49625697e3e7210b196693e079ce1789c847f8a4 GIT binary patch literal 4545 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 literal 0 HcmV?d00001 diff --git a/templates/World_Label_normal.png b/templates/World_Label_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..d731781629fa5838c837716e710cc810396fe69d GIT binary patch literal 3332 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 O0000 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 literal 5821 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 bool: + """Check if two bounding boxes' top-left corners are close.""" if bbox1 is None or bbox2 is None: return False - # Compare top-left coordinates (bbox[0], bbox[1]) return abs(bbox1[0] - bbox2[0]) <= tolerance and abs(bbox1[1] - bbox2[1]) <= tolerance -# --- Main Logic Functions --- +# ============================================================================== +# Detection Module +# ============================================================================== +class DetectionModule: + """Handles finding elements and states on the screen using image recognition.""" -def find_dialogue_bubbles(): - """ - Scan the screen for regular bubble corners and Bot bubble corners, and try to pair them. - Returns a list containing bounding boxes and whether they are Bot bubbles. - !!! The matching logic is very basic and needs significant improvement based on actual needs !!! - """ - all_bubbles_with_type = [] # Store (bbox, is_bot_flag) + def __init__(self, templates: Dict[str, str], confidence: float = CONFIDENCE_THRESHOLD, state_confidence: float = STATE_CONFIDENCE_THRESHOLD, region: Optional[Tuple[int, int, int, int]] = SCREENSHOT_REGION): + self.templates = templates + self.confidence = confidence + self.state_confidence = state_confidence + self.region = region + self._warned_paths = set() + print("DetectionModule initialized.") - # 1. Find all regular corners - tl_corners = find_template_on_screen(CORNER_TL_IMG, region=SCREENSHOT_REGION) - br_corners = find_template_on_screen(CORNER_BR_IMG, region=SCREENSHOT_REGION) - # tr_corners = find_template_on_screen(CORNER_TR_IMG, region=SCREENSHOT_REGION) # Not using TR/BL for now - # bl_corners = find_template_on_screen(CORNER_BL_IMG, region=SCREENSHOT_REGION) + def _find_template(self, template_key: str, confidence: Optional[float] = None, region: Optional[Tuple[int, int, int, int]] = None, grayscale: bool = False) -> List[Tuple[int, int]]: + """Internal helper to find a template by its key.""" + template_path = self.templates.get(template_key) + if not template_path: + print(f"Error: Template key '{template_key}' not found in provided templates.") + return [] - # 2. Find all Bot corners - bot_tl_corners = find_template_on_screen(BOT_CORNER_TL_IMG, region=SCREENSHOT_REGION) - bot_br_corners = find_template_on_screen(BOT_CORNER_BR_IMG, region=SCREENSHOT_REGION) - # bot_tr_corners = find_template_on_screen(BOT_CORNER_TR_IMG, region=SCREENSHOT_REGION) - # bot_bl_corners = find_template_on_screen(BOT_CORNER_BL_IMG, region=SCREENSHOT_REGION) + # Check if template file exists, warn only once + if not os.path.exists(template_path): + if template_path not in self._warned_paths: + print(f"Error: Template image doesn't exist: {template_path}") + self._warned_paths.add(template_path) + return [] - # 3. Try to match regular bubbles (using TL and BR) - processed_tls = set() # Track already matched TL indices - if tl_corners and br_corners: - for i, tl in enumerate(tl_corners): - if i in processed_tls: continue - potential_br = None - min_dist_sq = float('inf') - # Find the best BR corresponding to this TL (e.g., closest, or satisfying specific geometric constraints) - for j, br in enumerate(br_corners): - # Check if BR is in a reasonable range to the bottom-right of TL - if br[0] > tl[0] + 20 and br[1] > tl[1] + 10: # Assume minimum width/height - dist_sq = (br[0] - tl[0])**2 + (br[1] - tl[1])**2 - # Could add more conditions here, e.g., aspect ratio limits - if dist_sq < min_dist_sq: # Simple nearest-match - potential_br = br - min_dist_sq = dist_sq + locations = [] + current_region = region if region is not None else self.region + current_confidence = confidence if confidence is not None else self.confidence - if potential_br: - # Assuming we found matching TL and BR, define bounding box - bubble_bbox = (tl[0], tl[1], potential_br[0], potential_br[1]) - all_bubbles_with_type.append((bubble_bbox, False)) # Mark as non-Bot - processed_tls.add(i) # Mark this TL as used + try: + matches = pyautogui.locateAllOnScreen(template_path, region=current_region, confidence=current_confidence, grayscale=grayscale) + if matches: + for box in matches: + center_x = box.left + box.width // 2 + center_y = box.top + box.height // 2 + locations.append((center_x, center_y)) + # print(f"Found template '{template_key}' at {len(locations)} locations.") # Debug + return locations + except Exception as e: + print(f"Error finding template '{template_key}' ({template_path}): {e}") + return [] - # 4. Try to match Bot bubbles (using Bot TL and Bot BR) - processed_bot_tls = set() - if bot_tl_corners and bot_br_corners: - for i, tl in enumerate(bot_tl_corners): - if i in processed_bot_tls: continue - potential_br = None - min_dist_sq = float('inf') - for j, br in enumerate(bot_br_corners): - if br[0] > tl[0] + 20 and br[1] > tl[1] + 10: - dist_sq = (br[0] - tl[0])**2 + (br[1] - tl[1])**2 - if dist_sq < min_dist_sq: + def find_elements(self, template_keys: List[str], confidence: Optional[float] = None, region: Optional[Tuple[int, int, int, int]] = None) -> Dict[str, List[Tuple[int, int]]]: + """Find multiple templates by their keys.""" + results = {} + for key in template_keys: + results[key] = self._find_template(key, confidence=confidence, region=region) + return results + + def find_dialogue_bubbles(self) -> List[Tuple[Tuple[int, int, int, int], bool]]: + """ + Scan screen for regular and bot bubble corners and pair them. + Returns list of (bbox, is_bot_flag). Basic matching logic. + """ + all_bubbles_with_type = [] + + # Find corners using the internal helper + tl_corners = self._find_template('corner_tl') + br_corners = self._find_template('corner_br') + bot_tl_corners = self._find_template('bot_corner_tl') + bot_br_corners = self._find_template('bot_corner_br') + + # Match regular bubbles + processed_tls = set() + if tl_corners and br_corners: + for i, tl in enumerate(tl_corners): + if i in processed_tls: continue + potential_br = None + min_dist_sq = float('inf') + for j, br in enumerate(br_corners): + if br[0] > tl[0] + 20 and br[1] > tl[1] + 10: + dist_sq = (br[0] - tl[0])**2 + (br[1] - tl[1])**2 + if dist_sq < min_dist_sq: potential_br = br min_dist_sq = dist_sq - if potential_br: - bubble_bbox = (tl[0], tl[1], potential_br[0], potential_br[1]) - all_bubbles_with_type.append((bubble_bbox, True)) # Mark as Bot - processed_bot_tls.add(i) + if potential_br: + bubble_bbox = (tl[0], tl[1], potential_br[0], potential_br[1]) + all_bubbles_with_type.append((bubble_bbox, False)) + processed_tls.add(i) - # print(f"Found {len(all_bubbles_with_type)} potential bubbles.") #reduce printing - return all_bubbles_with_type + # Match Bot bubbles + processed_bot_tls = set() + if bot_tl_corners and bot_br_corners: + for i, tl in enumerate(bot_tl_corners): + if i in processed_bot_tls: continue + potential_br = None + min_dist_sq = float('inf') + for j, br in enumerate(bot_br_corners): + if br[0] > tl[0] + 20 and br[1] > tl[1] + 10: + dist_sq = (br[0] - tl[0])**2 + (br[1] - tl[1])**2 + if dist_sq < min_dist_sq: + potential_br = br + min_dist_sq = dist_sq + if potential_br: + bubble_bbox = (tl[0], tl[1], potential_br[0], potential_br[1]) + all_bubbles_with_type.append((bubble_bbox, True)) + processed_bot_tls.add(i) + return all_bubbles_with_type + + def find_keyword_in_region(self, region: Tuple[int, int, int, int]) -> Optional[Tuple[int, int]]: + """Look for keywords within a specified region.""" + if region[2] <= 0 or region[3] <= 0: return None # Invalid region width/height + + # Try lowercase + locations_lower = self._find_template('keyword_wolf_lower', region=region) + if locations_lower: + print(f"Found keyword (lowercase) in region {region}, position: {locations_lower[0]}") + return locations_lower[0] + + # Try uppercase + locations_upper = self._find_template('keyword_wolf_upper', region=region) + if locations_upper: + print(f"Found keyword (uppercase) in region {region}, position: {locations_upper[0]}") + return locations_upper[0] -def find_keyword_in_bubble(bubble_bbox): - """ - Look for the keywords "wolf" or "Wolf" images within the specified bubble area. - """ - x_min, y_min, x_max, y_max = bubble_bbox - width = x_max - x_min - height = y_max - y_min - if width <= 0 or height <= 0: - # print(f"Warning: Invalid bubble area {bubble_bbox}") #reduce printing return None - search_region = (x_min, y_min, width, height) - # print(f"Searching for keywords in region {search_region}...") #reduce printing - # 1. Try to find lowercase "wolf" - keyword_locations_lower = find_template_on_screen(KEYWORD_wolf_LOWER_IMG, region=search_region) - if keyword_locations_lower: - keyword_coords = keyword_locations_lower[0] - print(f"Found keyword (lowercase) in bubble {bubble_bbox}, position: {keyword_coords}") - return keyword_coords + def calculate_avatar_coords(self, bubble_bbox: Tuple[int, int, int, int], offset_x: int = AVATAR_OFFSET_X) -> Tuple[int, int]: + """Calculate avatar coordinates based on bubble top-left.""" + tl_x, tl_y = bubble_bbox[0], bubble_bbox[1] + avatar_x = tl_x + offset_x + avatar_y = tl_y # Assuming Y is same as top-left + # print(f"Calculated avatar coordinates: ({int(avatar_x)}, {int(avatar_y)})") # Reduce noise + return (int(avatar_x), int(avatar_y)) - # 2. If lowercase not found, try uppercase "Wolf" - keyword_locations_upper = find_template_on_screen(KEYWORD_Wolf_UPPER_IMG, region=search_region) - if keyword_locations_upper: - keyword_coords = keyword_locations_upper[0] - print(f"Found keyword (uppercase) in bubble {bubble_bbox}, position: {keyword_coords}") - return keyword_coords + def get_current_ui_state(self) -> str: + """Determine the current UI state based on visible elements.""" + # Check in order of specificity or likelihood + if self._find_template('profile_name_page', confidence=self.state_confidence): + return 'user_details' + if self._find_template('profile_page', confidence=self.state_confidence): + return 'profile_card' + # Add checks for world/private chat later + if self._find_template('world_chat', confidence=self.state_confidence): # Example + return 'world_chat' + if self._find_template('private_chat', confidence=self.state_confidence): # Example + return 'private_chat' + if self._find_template('chat_room', confidence=self.state_confidence): + return 'chat_room' # General chat room if others aren't found - # If neither found - return None + return 'unknown' -def find_avatar_for_bubble(bubble_bbox): - """Calculate avatar frame position based on bubble's top-left coordinates.""" - tl_x, tl_y = bubble_bbox[0], bubble_bbox[1] - # Adjust offset and Y-coordinate calculation based on actual layout - avatar_x = tl_x + AVATAR_OFFSET_X # Use updated offset - avatar_y = tl_y # Assume Y coordinate is same as top-left - print(f"Calculated avatar coordinates: ({int(avatar_x)}, {int(avatar_y)})") - return (avatar_x, avatar_y) +# ============================================================================== +# Interaction Module +# ============================================================================== +class InteractionModule: + """Handles performing actions on the UI like clicking, typing, clipboard.""" -def get_bubble_text(keyword_coords): - """ - Click on keyword position, simulate menu selection "Copy" or press Ctrl+C, and get text from clipboard. - """ - print(f"Attempting to copy @ {keyword_coords}..."); - original_clipboard = get_clipboard_text() or "" # Ensure not None - set_clipboard_text("___MCP_CLEAR___") # Use special marker to clear - time.sleep(0.1) # Brief wait for clipboard operation + def __init__(self, detector: DetectionModule, input_coords: Tuple[int, int] = (CHAT_INPUT_CENTER_X, CHAT_INPUT_CENTER_Y), input_template_key: Optional[str] = 'chat_input', send_button_key: str = 'send_button'): + self.detector = detector + self.default_input_coords = input_coords + self.input_template_key = input_template_key + self.send_button_key = send_button_key + print("InteractionModule initialized.") - # Click on keyword position - click_at(keyword_coords[0], keyword_coords[1]) - time.sleep(0.2) # Wait for possible menu or reaction - - # Try to find and click "Copy" menu item - copy_item_locations = find_template_on_screen(COPY_MENU_ITEM_IMG, confidence=0.7) - copied = False # Initialize copy state - if copy_item_locations: - copy_coords = copy_item_locations[0] - click_at(copy_coords[0], copy_coords[1]) - print("Clicked 'Copy' menu item.") - time.sleep(0.2) # Wait for copy operation to complete - copied = True # Mark copy operation as attempted (via click) - else: - print("'Copy' menu item not found. Attempting to simulate Ctrl+C.") - # --- Corrected try block --- + def click_at(self, x: int, y: int, button: str = 'left', clicks: int = 1, interval: float = 0.1, duration: float = 0.1): + """Safely click at specific coordinates.""" try: - pyautogui.hotkey('ctrl', 'c') - time.sleep(0.2) # Wait for copy operation to complete - print("Simulated Ctrl+C.") - copied = True # Mark copy operation as attempted (via hotkey) - except Exception as e_ctrlc: - print(f"Failed to simulate Ctrl+C: {e_ctrlc}") - copied = False # Ensure copied is False on failure - # --- End correction --- + print(f"Moving to and clicking at: ({x}, {y}), button: {button}, clicks: {clicks}") + pyautogui.moveTo(x, y, duration=duration) + pyautogui.click(button=button, clicks=clicks, interval=interval) + time.sleep(0.1) + except Exception as e: + print(f"Error clicking at coordinates ({x}, {y}): {e}") - # Check clipboard content - copied_text = get_clipboard_text() + def press_key(self, key: str, presses: int = 1, interval: float = 0.1): + """Press a specific key.""" + try: + print(f"Pressing key: {key} ({presses} times)") + for _ in range(presses): + pyautogui.press(key) + time.sleep(interval) + except Exception as e: + print(f"Error pressing key '{key}': {e}") - # Restore original clipboard - pyperclip.copy(original_clipboard) + def hotkey(self, *args): + """Press a key combination (e.g., 'ctrl', 'c').""" + try: + print(f"Pressing hotkey: {args}") + pyautogui.hotkey(*args) + time.sleep(0.1) # Short pause after hotkey + except Exception as e: + print(f"Error pressing hotkey {args}: {e}") - # Determine if copy was successful - if copied and copied_text and copied_text != "___MCP_CLEAR___": - print(f"Successfully copied text, length: {len(copied_text)}") - return copied_text.strip() # Return text with leading/trailing whitespace removed - else: - print("Error: Copy operation unsuccessful or clipboard content invalid.") - return None + def get_clipboard(self) -> Optional[str]: + """Get text from clipboard.""" + try: + return pyperclip.paste() + except Exception as e: + print(f"Error reading clipboard: {e}") + return None -def get_sender_name(avatar_coords): - """ - Click avatar, open profile card, click option, open user details, click copy name. - Uses state-based ESC cleanup logic. - """ - print(f"Attempting to get username from avatar {avatar_coords}...") - original_clipboard = get_clipboard_text() or "" - set_clipboard_text("___MCP_CLEAR___") - time.sleep(0.1) - sender_name = None # Initialize - success = False # Mark whether name retrieval was successful + def set_clipboard(self, text: str): + """Set clipboard text.""" + try: + pyperclip.copy(text) + except Exception as e: + print(f"Error writing to clipboard: {e}") - try: - # 1. Click avatar - click_at(avatar_coords[0], avatar_coords[1]) - time.sleep(.3) # Wait for profile card to appear + def copy_text_at(self, coords: Tuple[int, int]) -> Optional[str]: + """Attempt to copy text after clicking at given coordinates.""" + print(f"Attempting to copy text at {coords}...") + original_clipboard = self.get_clipboard() or "" + self.set_clipboard("___MCP_CLEAR___") + time.sleep(0.1) - # 2. Find and click option on profile card (triggers user details) - profile_option_locations = find_template_on_screen(PROFILE_OPTION_IMG, confidence=0.7) - if not profile_option_locations: - print("Error: User details option not found on profile card.") - # No need to raise exception here, let finally handle cleanup + self.click_at(coords[0], coords[1]) + time.sleep(0.2) # Wait for menu/reaction + + copied = False + # Try finding "Copy" menu item first + copy_item_locations = self.detector._find_template('copy_menu_item', confidence=0.7) # Use detector + if copy_item_locations: + copy_coords = copy_item_locations[0] + self.click_at(copy_coords[0], copy_coords[1]) + print("Clicked 'Copy' menu item.") + time.sleep(0.2) + copied = True else: - click_at(profile_option_locations[0][0], profile_option_locations[0][1]) - print("Clicked user details option.") - time.sleep(.3) # Wait for user details window to appear + print("'Copy' menu item not found. Attempting Ctrl+C.") + try: + self.hotkey('ctrl', 'c') + time.sleep(0.2) + print("Simulated Ctrl+C.") + copied = True + except Exception as e_ctrlc: + print(f"Failed to simulate Ctrl+C: {e_ctrlc}") + copied = False - # 3. Find and click "Copy Name" button in user details - copy_name_locations = find_template_on_screen(COPY_NAME_BUTTON_IMG, confidence=0.7) + copied_text = self.get_clipboard() + self.set_clipboard(original_clipboard) # Restore clipboard + + if copied and copied_text and copied_text != "___MCP_CLEAR___": + print(f"Successfully copied text, length: {len(copied_text)}") + return copied_text.strip() + else: + print("Error: Copy operation unsuccessful or clipboard content invalid.") + return None + + def retrieve_sender_name_interaction(self, avatar_coords: Tuple[int, int]) -> Optional[str]: + """ + Perform the sequence of actions to copy sender name, *without* cleanup. + Returns the name or None if failed. + """ + print(f"Attempting interaction to get username from avatar {avatar_coords}...") + original_clipboard = self.get_clipboard() or "" + self.set_clipboard("___MCP_CLEAR___") + time.sleep(0.1) + sender_name = None + + try: + # 1. Click avatar + self.click_at(avatar_coords[0], avatar_coords[1]) + time.sleep(0.3) # Wait for profile card + + # 2. Find and click profile option + profile_option_locations = self.detector._find_template('profile_option', confidence=0.7) + if not profile_option_locations: + print("Error: User details option not found on profile card.") + return None # Fail early if critical step missing + self.click_at(profile_option_locations[0][0], profile_option_locations[0][1]) + print("Clicked user details option.") + time.sleep(0.3) # Wait for user details window + + # 3. Find and click "Copy Name" button + copy_name_locations = self.detector._find_template('copy_name_button', confidence=0.7) if not copy_name_locations: print("Error: 'Copy Name' button not found in user details.") + return None # Fail early + self.click_at(copy_name_locations[0][0], copy_name_locations[0][1]) + print("Clicked 'Copy Name' button.") + time.sleep(0.1) + + # 4. Get name from clipboard + copied_name = self.get_clipboard() + if copied_name and copied_name != "___MCP_CLEAR___": + print(f"Successfully copied username: {copied_name}") + sender_name = copied_name.strip() else: - click_at(copy_name_locations[0][0], copy_name_locations[0][1]) - print("Clicked 'Copy Name' button.") - time.sleep(0.1) # Wait for copy to complete - copied_name = get_clipboard_text() - if copied_name and copied_name != "___MCP_CLEAR___": - print(f"Successfully copied username: {copied_name}") - sender_name = copied_name.strip() # Store successfully copied name - success = True # Mark success - else: - print("Error: Clipboard content unchanged or empty, failed to copy username.") + print("Error: Clipboard content invalid after clicking copy name.") + sender_name = None - # Regardless of success above, return sender_name (might be None) - return sender_name + return sender_name - except Exception as e: - print(f"Error during username retrieval process: {e}") - import traceback - traceback.print_exc() - return None # Return None to indicate failure - finally: - # --- State-based cleanup logic --- - print("Performing cleanup: Attempting to press ESC to return to chat interface based on screen state...") - max_esc_attempts = 4 # Increase attempt count just in case - returned_to_chat = False - for attempt in range(max_esc_attempts): - print(f"Cleanup attempt #{attempt + 1}/{max_esc_attempts}") - time.sleep(0.2) # Short wait before each attempt + except Exception as e: + print(f"Error during username retrieval interaction: {e}") + import traceback + traceback.print_exc() + return None + finally: + # Restore clipboard regardless of success/failure + self.set_clipboard(original_clipboard) + # NO cleanup logic here - should be handled by coordinator - # First check if already returned to chat room - # Using lower confidence for state checks may be more stable - if find_template_on_screen(CHAT_ROOM_IMG, confidence=STATE_CONFIDENCE_THRESHOLD): - print("Chat room interface detected, cleanup complete.") - returned_to_chat = True - break # Already returned, exit loop - - # Check if in user details page - elif find_template_on_screen(PROFILE_NAME_PAGE_IMG, confidence=STATE_CONFIDENCE_THRESHOLD): - print("User details page detected, pressing ESC...") - pyautogui.press('esc') - time.sleep(0.2) # Wait for UI response - continue # Continue to next loop iteration - - # Check if in profile card page - elif find_template_on_screen(PROFILE_PAGE_IMG, confidence=STATE_CONFIDENCE_THRESHOLD): - print("Profile card page detected, pressing ESC...") - pyautogui.press('esc') - time.sleep(0.2) # Wait for UI response - continue # Continue to next loop iteration - - else: - # Cannot identify current state - print("No known page state detected.") - if attempt < max_esc_attempts - 1: - print("Trying one ESC press as fallback...") - pyautogui.press('esc') - time.sleep(0.2) # Wait for response - else: - print("Maximum attempts reached, stopping cleanup.") - break # Exit loop - - if not returned_to_chat: - print("Warning: Could not confirm return to chat room interface via state detection.") - # --- End of new cleanup logic --- - - # Ensure clipboard is restored - pyperclip.copy(original_clipboard) - - -def paste_and_send_reply(reply_text): - """ - Click chat input box, paste response, click send button or press Enter. - """ - print("Preparing to send response...") - # --- Corrected if statement --- - if not reply_text: - print("Error: Response content is empty, cannot send.") - return False - # --- End correction --- - - input_coords = None - if os.path.exists(CHAT_INPUT_IMG): - input_locations = find_template_on_screen(CHAT_INPUT_IMG, confidence=0.7) - if input_locations: - input_coords = input_locations[0] - print(f"Found input box position via image: {input_coords}") - else: - print("Warning: Input box not found via image, using default coordinates.") - input_coords = (CHAT_INPUT_CENTER_X, CHAT_INPUT_CENTER_Y) - else: - print("Warning: Input box template image doesn't exist, using default coordinates.") - input_coords = (CHAT_INPUT_CENTER_X, CHAT_INPUT_CENTER_Y) - - click_at(input_coords[0], input_coords[1]) - time.sleep(0.3) - - print("Pasting response...") - set_clipboard_text(reply_text) - time.sleep(0.1) - try: - pyautogui.hotkey('ctrl', 'v') - time.sleep(0.5) - print("Pasted.") - except Exception as e: - print(f"Error pasting response: {e}") - return False - - send_button_locations = find_template_on_screen(SEND_BUTTON_IMG, confidence=0.7) - if send_button_locations: - send_coords = send_button_locations[0] - click_at(send_coords[0], send_coords[1]) - print("Clicked send button.") - time.sleep(0.1) - return True - else: - print("Send button not found. Attempting to press Enter.") - try: - pyautogui.press('enter') - print("Pressed Enter.") - time.sleep(0.5) - return True - except Exception as e_enter: - print(f"Error pressing Enter: {e_enter}") + def send_chat_message(self, reply_text: str) -> bool: + """Paste text into chat input and send it.""" + print("Preparing to send response...") + if not reply_text: + print("Error: Response content is empty, cannot send.") return False + # Find input box coordinates + input_coords = self.default_input_coords # Fallback + if self.input_template_key and self.detector.templates.get(self.input_template_key): + input_locations = self.detector._find_template(self.input_template_key, confidence=0.7) + if input_locations: + input_coords = input_locations[0] + print(f"Found input box position via image: {input_coords}") + else: + print(f"Warning: Input box template '{self.input_template_key}' not found, using default coordinates.") + else: + print("Warning: Input box template key not set or image missing, using default coordinates.") -# --- Main Monitoring and Triggering Logic --- -recent_texts = collections.deque(maxlen=RECENT_TEXT_HISTORY_MAXLEN) -last_processed_bubble_bbox = None + # Click input, paste, send + self.click_at(input_coords[0], input_coords[1]) + time.sleep(0.3) -def monitor_chat_for_trigger(trigger_queue: queue.Queue): # Using standard queue + print("Pasting response...") + self.set_clipboard(reply_text) + time.sleep(0.1) + try: + self.hotkey('ctrl', 'v') + time.sleep(0.5) + print("Pasted.") + except Exception as e: + print(f"Error pasting response: {e}") + return False + + # Try clicking send button first + send_button_locations = self.detector._find_template(self.send_button_key, confidence=0.7) + if send_button_locations: + send_coords = send_button_locations[0] + self.click_at(send_coords[0], send_coords[1]) + print("Clicked send button.") + time.sleep(0.1) + return True + else: + # Fallback to pressing Enter + print("Send button not found. Attempting to press Enter.") + try: + self.press_key('enter') + print("Pressed Enter.") + time.sleep(0.5) + return True + except Exception as e_enter: + print(f"Error pressing Enter: {e_enter}") + return False + +# ============================================================================== +# Coordinator Logic (Placeholder - To be implemented in main.py) +# ============================================================================== + +# --- State-based Cleanup Function (To be called by Coordinator) --- +def perform_state_cleanup(detector: DetectionModule, interactor: InteractionModule, max_attempts: int = 4) -> bool: """ - Continuously monitor chat area, look for bubbles containing keywords and put trigger info in Queue. + Attempt to return to the main chat room interface by pressing ESC based on detected state. + Returns True if confirmed back in chat room, False otherwise. """ - global last_processed_bubble_bbox - print(f"\n--- Starting chat room monitoring (UI Thread) ---") - # No longer need to get loop + print("Performing cleanup: Attempting to press ESC to return to chat interface...") + returned_to_chat = False + for attempt in range(max_attempts): + print(f"Cleanup attempt #{attempt + 1}/{max_attempts}") + time.sleep(0.2) + + current_state = detector.get_current_ui_state() + print(f"Detected state: {current_state}") + + if current_state == 'chat_room' or current_state == 'world_chat' or current_state == 'private_chat': # Adjust as needed + print("Chat room interface detected, cleanup complete.") + returned_to_chat = True + break + elif current_state == 'user_details' or current_state == 'profile_card': + print(f"{current_state.replace('_', ' ').title()} detected, pressing ESC...") + interactor.press_key('esc') + time.sleep(0.3) # Wait longer for UI response after ESC + continue + else: # Unknown state + print("Unknown page state detected.") + if attempt < max_attempts - 1: + print("Trying one ESC press as fallback...") + interactor.press_key('esc') + time.sleep(0.3) + else: + print("Maximum attempts reached, stopping cleanup.") + break + + if not returned_to_chat: + print("Warning: Could not confirm return to chat room interface via state detection.") + return returned_to_chat + + +# --- UI Monitoring Loop Function (To be run in a separate thread) --- +def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queue): + """ + Continuously monitors the UI, detects triggers, performs interactions, + puts trigger data into trigger_queue, and processes commands from command_queue. + """ + print("\n--- Starting UI Monitoring Loop (Thread) ---") + + # --- Initialization (Instantiate modules within the thread) --- + # Load templates directly using constants defined in this file for now + # Consider passing config or a template loader object in the future + templates = { + 'corner_tl': CORNER_TL_IMG, 'corner_br': CORNER_BR_IMG, + 'bot_corner_tl': BOT_CORNER_TL_IMG, 'bot_corner_br': BOT_CORNER_BR_IMG, + 'keyword_wolf_lower': KEYWORD_wolf_LOWER_IMG, 'keyword_wolf_upper': KEYWORD_Wolf_UPPER_IMG, + 'copy_menu_item': COPY_MENU_ITEM_IMG, 'profile_option': PROFILE_OPTION_IMG, + 'copy_name_button': COPY_NAME_BUTTON_IMG, 'send_button': SEND_BUTTON_IMG, + 'chat_input': CHAT_INPUT_IMG, 'profile_name_page': PROFILE_NAME_PAGE_IMG, + 'profile_page': PROFILE_PAGE_IMG, 'chat_room': CHAT_ROOM_IMG, + 'world_chat': WORLD_CHAT_IMG, 'private_chat': PRIVATE_CHAT_IMG # Add other templates as needed + } + # Use default confidence/region settings from constants + detector = DetectionModule(templates, confidence=CONFIDENCE_THRESHOLD, state_confidence=STATE_CONFIDENCE_THRESHOLD, region=SCREENSHOT_REGION) + # Use default input coords/keys from constants + interactor = InteractionModule(detector, input_coords=(CHAT_INPUT_CENTER_X, CHAT_INPUT_CENTER_Y), input_template_key='chat_input', send_button_key='send_button') + + # --- State Management (Local to this monitoring thread) --- + last_processed_bubble_bbox = None + recent_texts = collections.deque(maxlen=RECENT_TEXT_HISTORY_MAXLEN) # Context-specific history needed while True: + # --- Process Commands First (Non-blocking) --- try: - all_bubbles_with_type = find_dialogue_bubbles() - if not all_bubbles_with_type: time.sleep(2); continue - other_bubbles_bboxes = [bbox for bbox, is_bot in all_bubbles_with_type if not is_bot] - if not other_bubbles_bboxes: time.sleep(2); continue - target_bubble = max(other_bubbles_bboxes, key=lambda b: b[3]) - if are_bboxes_similar(target_bubble, last_processed_bubble_bbox): time.sleep(2); continue + command_data = command_queue.get_nowait() # Check for commands without blocking + action = command_data.get('action') + if action == 'send_reply': + text_to_send = command_data.get('text') + if text_to_send: + print(f"UI Thread: Received command to send reply: '{text_to_send[:50]}...'") + interactor.send_chat_message(text_to_send) + else: + print("UI Thread: Received send_reply command with no text.") + else: + print(f"UI Thread: Received unknown command: {action}") + except queue.Empty: + pass # No command waiting, continue with monitoring + except Exception as cmd_err: + print(f"UI Thread: Error processing command queue: {cmd_err}") + + # --- Then Perform UI Monitoring --- + try: + # 1. Detect Bubbles + all_bubbles = detector.find_dialogue_bubbles() + if not all_bubbles: time.sleep(2); continue + + # Filter out bot bubbles, find newest non-bot bubble (example logic) + other_bubbles = [bbox for bbox, is_bot in all_bubbles if not is_bot] + if not other_bubbles: time.sleep(2); continue + # Simple logic: assume lowest bubble is newest (might need improvement) + target_bubble = max(other_bubbles, key=lambda b: b[3]) # b[3] is y_max + + # 2. Check for Duplicates (Position & Content) + if are_bboxes_similar(target_bubble, last_processed_bubble_bbox): + time.sleep(2); continue + + # 3. Detect Keyword in Bubble + bubble_region = (target_bubble[0], target_bubble[1], target_bubble[2]-target_bubble[0], target_bubble[3]-target_bubble[1]) + keyword_coords = detector.find_keyword_in_region(bubble_region) - keyword_coords = find_keyword_in_bubble(target_bubble) if keyword_coords: print(f"\n!!! Keyword detected in bubble {target_bubble} !!!") - bubble_text = get_bubble_text(keyword_coords) # Using corrected version - if not bubble_text: print("Error: Could not get dialogue content."); last_processed_bubble_bbox = target_bubble; continue - if bubble_text in recent_texts: print(f"Content '{bubble_text[:30]}...' in recent history, skipping."); last_processed_bubble_bbox = target_bubble; continue - print(">>> New trigger event <<<"); last_processed_bubble_bbox = target_bubble; recent_texts.append(bubble_text) - avatar_coords = find_avatar_for_bubble(target_bubble) - sender_name = get_sender_name(avatar_coords) # Using version with state cleanup - if not sender_name: print("Error: Could not get sender name, aborting processing."); continue + # 4. Interact: Get Bubble Text + bubble_text = interactor.copy_text_at(keyword_coords) + if not bubble_text: + print("Error: Could not get dialogue content.") + last_processed_bubble_bbox = target_bubble # Mark as processed even if failed + perform_state_cleanup(detector, interactor) # Attempt cleanup after failed copy + continue - print("\n>>> Putting trigger info in Queue <<<"); print(f" Sender: {sender_name}"); print(f" Content: {bubble_text[:100]}...") + # Check recent text history (needs context awareness) + if bubble_text in recent_texts: + print(f"Content '{bubble_text[:30]}...' in recent history, skipping.") + last_processed_bubble_bbox = target_bubble + continue + + print(">>> New trigger event <<<") + last_processed_bubble_bbox = target_bubble + recent_texts.append(bubble_text) + + # 5. Interact: Get Sender Name + avatar_coords = detector.calculate_avatar_coords(target_bubble) + sender_name = interactor.retrieve_sender_name_interaction(avatar_coords) + + # 6. Perform Cleanup (Crucial after potentially leaving chat screen) + cleanup_successful = perform_state_cleanup(detector, interactor) + if not cleanup_successful: + print("Error: Failed to return to chat screen after getting name. Aborting trigger.") + continue # Skip putting in queue if cleanup failed + + if not sender_name: + print("Error: Could not get sender name, aborting processing.") + continue # Already cleaned up, just skip + + # 7. Send Trigger Info to Main Thread/Async Loop + print("\n>>> Putting trigger info in Queue <<<") + print(f" Sender: {sender_name}") + print(f" Content: {bubble_text[:100]}...") try: - # --- Using queue.put (synchronous) --- data_to_send = {'sender': sender_name, 'text': bubble_text} - trigger_queue.put(data_to_send) # Directly put into standard Queue + trigger_queue.put(data_to_send) # Put in the queue for main loop print("Trigger info placed in Queue.") - except Exception as q_err: print(f"Error putting data in Queue: {q_err}") - print("--- Single trigger processing complete ---"); time.sleep(1) - time.sleep(1.5) - except KeyboardInterrupt: print("\nMonitoring interrupted."); break - except Exception as e: print(f"Unknown error in monitoring loop: {e}"); import traceback; traceback.print_exc(); print("Waiting 5 seconds before retry..."); time.sleep(5) + except Exception as q_err: + print(f"Error putting data in Queue: {q_err}") -# if __name__ == '__main__': # Keep commented, typically called from main.py + print("--- Single trigger processing complete ---") + time.sleep(1) # Pause after successful trigger + + time.sleep(1.5) # Polling interval + + except KeyboardInterrupt: + print("\nMonitoring interrupted.") + break + except Exception as e: + print(f"Unknown error in monitoring loop: {e}") + import traceback + traceback.print_exc() + # Attempt cleanup in case of unexpected error during interaction + print("Attempting cleanup after unexpected error...") + perform_state_cleanup(detector, interactor) + print("Waiting 5 seconds before retry...") + time.sleep(5) + +# Note: The old monitor_chat_for_trigger function is replaced by the example_coordinator_loop. +# The actual UI monitoring thread started in main.py should call a function like this example loop. +# The main async loop in main.py will handle getting items from the queue and interacting with the LLM. + +# if __name__ == '__main__': +# # This module is not meant to be run directly after refactoring. +# # Initialization and coordination happen in main.py. # pass