Added the function of dismissing positions. Improved the stability of chat bubble detection.
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
llm_debug.log
|
llm_debug.log
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
debug_screenshots/
|
||||||
@ -75,10 +75,10 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
|||||||
|
|
||||||
系統使用基於圖像辨識的方法監控遊戲聊天界面:
|
系統使用基於圖像辨識的方法監控遊戲聊天界面:
|
||||||
|
|
||||||
1. **泡泡檢測**:通過辨識聊天泡泡的角落圖案定位聊天訊息,區分一般用戶與機器人
|
1. **泡泡檢測**:通過辨識聊天泡泡的左上角 (TL) 和右下角 (BR) 角落圖案定位聊天訊息。系統能區分一般用戶泡泡和機器人泡泡。**為了適應玩家可能使用的不同聊天泡泡外觀 (skin),一般用戶泡泡的偵測機制已被擴充,可以同時尋找多組不同的角落模板 (例如 `corner_tl_type2.png`, `corner_br_type2.png` 等),提高了對自訂外觀的兼容性。機器人泡泡目前僅偵測預設的角落模板。**
|
||||||
2. **關鍵字檢測**:在泡泡區域內搜尋 "wolf" 或 "Wolf" 關鍵字圖像
|
2. **關鍵字檢測**:在泡泡區域內搜尋 "wolf" 或 "Wolf" 關鍵字圖像
|
||||||
3. **內容獲取**:點擊關鍵字位置,使用剪貼板複製聊天內容
|
3. **內容獲取**:點擊關鍵字位置,使用剪貼板複製聊天內容
|
||||||
4. **發送者識別**:通過點擊頭像,導航菜單,複製用戶名稱
|
4. **發送者識別**:**關鍵步驟** - 系統會根據**偵測到關鍵字的那個特定聊天泡泡**的左上角座標,計算出頭像的點擊位置(目前水平偏移量為 -55 像素)。這確保了點擊的是觸發訊息的發送者頭像,而不是其他位置的頭像。接著通過點擊計算出的頭像位置,導航菜單,最終複製用戶名稱。
|
||||||
5. **防重複處理**:使用位置比較和內容歷史記錄防止重複回應
|
5. **防重複處理**:使用位置比較和內容歷史記錄防止重複回應
|
||||||
|
|
||||||
#### LLM 整合
|
#### LLM 整合
|
||||||
@ -167,6 +167,31 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
|||||||
|
|
||||||
這些優化確保了即使在複雜工具調用後,Wolfhart 也能保持角色一致性,並提供合適的回應。無效回應不再發送到遊戲,提高了用戶體驗。
|
這些優化確保了即使在複雜工具調用後,Wolfhart 也能保持角色一致性,並提供合適的回應。無效回應不再發送到遊戲,提高了用戶體驗。
|
||||||
|
|
||||||
|
## 最近改進(2025-04-18)
|
||||||
|
|
||||||
|
### 支援多種一般聊天泡泡外觀,並修正先前錯誤配置
|
||||||
|
|
||||||
|
- **UI 互動模塊 (`ui_interaction.py`)**:
|
||||||
|
- **修正**:先前錯誤地將多外觀支援應用於機器人泡泡。現已修正 `find_dialogue_bubbles` 函數,使其能夠載入並搜尋多組**一般用戶**泡泡的角落模板(例如 `corner_tl_type2.png`, `corner_br_type2.png` 等)。
|
||||||
|
- 允許任何類型的一般用戶左上角與任何類型的一般用戶右下角進行配對,只要符合幾何條件。
|
||||||
|
- 機器人泡泡的偵測恢復為僅使用預設的 `bot_corner_tl.png` 和 `bot_corner_br.png` 模板。
|
||||||
|
- 這提高了對使用了自訂聊天泡泡外觀的**一般玩家**訊息的偵測能力。
|
||||||
|
- **模板文件**:
|
||||||
|
- 在 `ui_interaction.py` 中為一般角落定義了新類型模板的路徑(`_type2`, `_type3`)。
|
||||||
|
- **注意:** 需要在 `templates` 資料夾中實際添加對應的 `corner_tl_type2.png`, `corner_br_type2.png` 等圖片檔案才能生效。
|
||||||
|
- **文件更新 (`ClaudeCode.md`)**:
|
||||||
|
- 在「技術實現」部分更新了泡泡檢測的說明。
|
||||||
|
- 添加了此「最近改進」條目,並修正了先前的描述。
|
||||||
|
|
||||||
|
### 頭像點擊偏移量調整
|
||||||
|
|
||||||
|
- **UI 互動模塊 (`ui_interaction.py`)**:
|
||||||
|
- 將 `AVATAR_OFFSET_X` 常數的值從 `-50` 調整為 `-55`。
|
||||||
|
- 這統一了常規關鍵字觸發流程和 `remove_user_position` 功能中計算頭像點擊位置時使用的水平偏移量。
|
||||||
|
- **文件更新 (`ClaudeCode.md`)**:
|
||||||
|
- 在「技術實現」的「發送者識別」部分強調了點擊位置是相對於觸發泡泡計算的,並註明了新的偏移量。
|
||||||
|
- 添加了此「最近改進」條目。
|
||||||
|
|
||||||
## 開發建議
|
## 開發建議
|
||||||
|
|
||||||
### 優化方向
|
### 優化方向
|
||||||
|
|||||||
32
config.py
@ -27,24 +27,24 @@ exa_config_dict = {"exaApiKey": EXA_API_KEY if EXA_API_KEY else "YOUR_EXA_KEY_MI
|
|||||||
# For cmd /c on Windows, embedding escaped JSON often works like this:
|
# For cmd /c on Windows, embedding escaped JSON often works like this:
|
||||||
exa_config_arg_string = json.dumps(json.dumps(exa_config_dict)) # Double dump for cmd escaping? Or just one? Test needed.
|
exa_config_arg_string = json.dumps(json.dumps(exa_config_dict)) # Double dump for cmd escaping? Or just one? Test needed.
|
||||||
# Let's try single dump first, often sufficient if passed correctly by subprocess
|
# Let's try single dump first, often sufficient if passed correctly by subprocess
|
||||||
exa_config_arg_string_single_dump = json.dumps(exa_config_dict)
|
exa_config_arg_string_single_dump = json.dumps(exa_config_dict) # Use this one
|
||||||
|
|
||||||
# --- MCP Server Configuration ---
|
# --- MCP Server Configuration ---
|
||||||
MCP_SERVERS = {
|
MCP_SERVERS = {
|
||||||
"exa": {
|
# "exa": { # Temporarily commented out to prevent blocking startup
|
||||||
"command": "cmd",
|
# "command": "cmd",
|
||||||
"args": [
|
# "args": [
|
||||||
"/c",
|
# "/c",
|
||||||
"npx",
|
# "npx",
|
||||||
"-y",
|
# "-y",
|
||||||
"@smithery/cli@latest",
|
# "@smithery/cli@latest",
|
||||||
"run",
|
# "run",
|
||||||
"exa",
|
# "exa",
|
||||||
"--config",
|
# "--config",
|
||||||
# Pass the dynamically created config string with the environment variable key
|
# # Pass the dynamically created config string with the environment variable key
|
||||||
exa_config_arg_string # Use the properly escaped variable
|
# exa_config_arg_string_single_dump # Use the single dump variable
|
||||||
],
|
# ],
|
||||||
},
|
# },
|
||||||
"servers": {
|
"servers": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": [
|
"args": [
|
||||||
@ -71,5 +71,5 @@ WINDOW_TITLE = "Last War-Survival Game"
|
|||||||
|
|
||||||
# --- Print loaded keys for verification (Optional - BE CAREFUL!) ---
|
# --- Print loaded keys for verification (Optional - BE CAREFUL!) ---
|
||||||
# print(f"DEBUG: Loaded OPENAI_API_KEY: {'*' * (len(OPENAI_API_KEY) - 4) + OPENAI_API_KEY[-4:] if OPENAI_API_KEY else 'Not Found'}")
|
# print(f"DEBUG: Loaded OPENAI_API_KEY: {'*' * (len(OPENAI_API_KEY) - 4) + OPENAI_API_KEY[-4:] if OPENAI_API_KEY else 'Not Found'}")
|
||||||
# print(f"DEBUG: Loaded EXA_API_KEY: {'*' * (len(EXA_API_KEY) - 4) + EXA_API_KEY[-4:] if EXA_API_KEY else 'Not Found'}")
|
print(f"DEBUG: Loaded EXA_API_KEY: {'*' * (len(EXA_API_KEY) - 4) + EXA_API_KEY[-4:] if EXA_API_KEY else 'Not Found'}") # Uncommented Exa key check
|
||||||
# print(f"DEBUG: Exa args: {MCP_SERVERS['exa']['args']}")
|
# print(f"DEBUG: Exa args: {MCP_SERVERS['exa']['args']}")
|
||||||
@ -165,10 +165,15 @@ You MUST respond in the following JSON format:
|
|||||||
Parameters: `names` (array of strings)
|
Parameters: `names` (array of strings)
|
||||||
Usage: Access specific entities you know exist in the graph.
|
Usage: Access specific entities you know exist in the graph.
|
||||||
|
|
||||||
|
**Game Actions:**
|
||||||
|
- `remove_position`: Initiate the process to remove a user's assigned position/role.
|
||||||
|
Parameters: (none) - The context (triggering message) is handled separately.
|
||||||
|
Usage: Use ONLY when the user explicitly requests a position removal AND you, as Wolfhart, decide to grant the request based on the interaction's tone, politeness, and perceived intent (e.g., not malicious or a prank). Your decision should reflect Wolfhart's personality (calm, strategic, potentially dismissive of rudeness or foolishness). If you decide to remove the position, include this command alongside your dialogue response.
|
||||||
|
|
||||||
3. `thoughts` (OPTIONAL): Your internal analysis that won't be shown to users. Use this for your reasoning process.
|
3. `thoughts` (OPTIONAL): Your internal analysis that won't be shown to users. Use this for your reasoning process.
|
||||||
- Think about whether you need to use memory tools or web search
|
- Think about whether you need to use memory tools or web search.
|
||||||
- Analyze the user's question and determine what information is needed
|
- Analyze the user's message: Is it a request to remove a position? If so, evaluate its politeness and intent from Wolfhart's perspective. Decide whether to issue the `remove_position` command.
|
||||||
- Plan your approach before responding
|
- Plan your approach before responding.
|
||||||
|
|
||||||
**VERY IMPORTANT Instructions:**
|
**VERY IMPORTANT Instructions:**
|
||||||
|
|
||||||
@ -691,4 +696,3 @@ async def _execute_single_tool_call(tool_call, mcp_sessions, available_mcp_tools
|
|||||||
f"Tool: {function_name}\nFormatted Response: {json.dumps(response, ensure_ascii=False, indent=2)}")
|
f"Tool: {function_name}\nFormatted Response: {json.dumps(response, ensure_ascii=False, indent=2)}")
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
35
main.py
@ -227,12 +227,15 @@ async def run_main_with_exit_stack():
|
|||||||
|
|
||||||
sender_name = trigger_data.get('sender')
|
sender_name = trigger_data.get('sender')
|
||||||
bubble_text = trigger_data.get('text')
|
bubble_text = trigger_data.get('text')
|
||||||
|
bubble_region = trigger_data.get('bubble_region') # <-- Extract bubble_region
|
||||||
print(f"\n--- Received trigger from UI ---")
|
print(f"\n--- Received trigger from UI ---")
|
||||||
print(f" Sender: {sender_name}")
|
print(f" Sender: {sender_name}")
|
||||||
print(f" Content: {bubble_text[:100]}...")
|
print(f" Content: {bubble_text[:100]}...")
|
||||||
|
if bubble_region:
|
||||||
|
print(f" Bubble Region: {bubble_region}") # <-- Log bubble_region
|
||||||
|
|
||||||
if not sender_name or not bubble_text:
|
if not sender_name or not bubble_text: # bubble_region is optional context, don't fail if missing
|
||||||
print("Warning: Received incomplete trigger data, skipping.")
|
print("Warning: Received incomplete trigger data (missing sender or text), skipping.")
|
||||||
# No task_done needed for standard queue
|
# No task_done needed for standard queue
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -257,10 +260,30 @@ async def run_main_with_exit_stack():
|
|||||||
print(f"Processing {len(commands)} command(s)...")
|
print(f"Processing {len(commands)} command(s)...")
|
||||||
for cmd in commands:
|
for cmd in commands:
|
||||||
cmd_type = cmd.get("type", "")
|
cmd_type = cmd.get("type", "")
|
||||||
cmd_params = cmd.get("parameters", {})
|
cmd_params = cmd.get("parameters", {}) # Parameters might be empty for remove_position
|
||||||
# 預留位置:在這裡添加命令處理邏輯
|
|
||||||
print(f"Command type: {cmd_type}, parameters: {cmd_params}")
|
# --- Command Processing ---
|
||||||
# TODO: 實現各類命令的處理邏輯
|
if cmd_type == "remove_position":
|
||||||
|
if bubble_region: # Check if we have the context
|
||||||
|
print("Sending 'remove_position' command to UI thread...")
|
||||||
|
command_to_send = {
|
||||||
|
'action': 'remove_position',
|
||||||
|
'trigger_bubble_region': bubble_region # Pass the region
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
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 remove_position command in queue: {q_err}")
|
||||||
|
else:
|
||||||
|
print("Error: Cannot process 'remove_position' command without bubble_region context.")
|
||||||
|
# Add other command handling here if needed
|
||||||
|
# elif cmd_type == "some_other_command":
|
||||||
|
# # Handle other commands
|
||||||
|
# pass
|
||||||
|
else:
|
||||||
|
print(f"Received unhandled command type: {cmd_type}, parameters: {cmd_params}")
|
||||||
|
# --- End Command Processing ---
|
||||||
|
|
||||||
# 記錄思考過程 (如果有的話)
|
# 記錄思考過程 (如果有的話)
|
||||||
thoughts = bot_response_data.get("thoughts", "")
|
thoughts = bot_response_data.get("thoughts", "")
|
||||||
|
|||||||
BIN
templates/base.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
templates/capitol/black_arrow_down.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
templates/capitol/capitol_#11.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
templates/capitol/close_button.png
Normal file
|
After Width: | Height: | Size: 418 B |
BIN
templates/capitol/confirm.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
templates/capitol/dismiss.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
templates/capitol/page_DEVELOPMENT.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
templates/capitol/page_INTERIOR.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
templates/capitol/page_SCIENCE.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
templates/capitol/page_SECURITY.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
templates/capitol/page_STRATEGY.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
templates/capitol/position_development.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
templates/capitol/position_interior.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
templates/capitol/position_science.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
templates/capitol/position_security.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
templates/capitol/position_strategy.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
templates/capitol/president_title.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
templates/capitol/president_title1.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
templates/capitol/president_title2.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
templates/corner_br_type2.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
templates/corner_br_type3.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 250 B |
BIN
templates/corner_tl_type2.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
templates/corner_tl_type3.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
templates/keyword_wolf_lower_type3.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
templates/keyword_wolf_upper_type3.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
templates/positions/development.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
templates/positions/interior.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
templates/positions/science.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
templates/positions/security.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
templates/positions/strategy.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
@ -19,19 +19,41 @@ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||||||
TEMPLATE_DIR = os.path.join(SCRIPT_DIR, "templates")
|
TEMPLATE_DIR = os.path.join(SCRIPT_DIR, "templates")
|
||||||
os.makedirs(TEMPLATE_DIR, exist_ok=True)
|
os.makedirs(TEMPLATE_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# --- Debugging ---
|
||||||
|
DEBUG_SCREENSHOT_DIR = os.path.join(SCRIPT_DIR, "debug_screenshots")
|
||||||
|
MAX_DEBUG_SCREENSHOTS = 5
|
||||||
|
os.makedirs(DEBUG_SCREENSHOT_DIR, exist_ok=True)
|
||||||
|
# --- End Debugging ---
|
||||||
|
|
||||||
# --- Template Paths (Consider moving to config.py or loading dynamically) ---
|
# --- Template Paths (Consider moving to config.py or loading dynamically) ---
|
||||||
# Bubble Corners
|
# Bubble Corners
|
||||||
CORNER_TL_IMG = os.path.join(TEMPLATE_DIR, "corner_tl.png")
|
CORNER_TL_IMG = os.path.join(TEMPLATE_DIR, "corner_tl.png")
|
||||||
CORNER_TR_IMG = os.path.join(TEMPLATE_DIR, "corner_tr.png")
|
# CORNER_TR_IMG = os.path.join(TEMPLATE_DIR, "corner_tr.png") # Unused
|
||||||
CORNER_BL_IMG = os.path.join(TEMPLATE_DIR, "corner_bl.png")
|
# CORNER_BL_IMG = os.path.join(TEMPLATE_DIR, "corner_bl.png") # Unused
|
||||||
CORNER_BR_IMG = os.path.join(TEMPLATE_DIR, "corner_br.png")
|
CORNER_BR_IMG = os.path.join(TEMPLATE_DIR, "corner_br.png")
|
||||||
|
# --- Additional Regular Bubble Types (Skins) ---
|
||||||
|
CORNER_TL_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "corner_tl_type2.png") # Added
|
||||||
|
CORNER_BR_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "corner_br_type2.png") # Added
|
||||||
|
CORNER_TL_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "corner_tl_type3.png") # Added
|
||||||
|
CORNER_BR_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "corner_br_type3.png") # Added
|
||||||
|
# --- End Additional Regular Types ---
|
||||||
BOT_CORNER_TL_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tl.png")
|
BOT_CORNER_TL_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tl.png")
|
||||||
BOT_CORNER_TR_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tr.png")
|
# BOT_CORNER_TR_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tr.png") # Unused
|
||||||
BOT_CORNER_BL_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_bl.png")
|
# BOT_CORNER_BL_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_bl.png") # Unused
|
||||||
BOT_CORNER_BR_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_br.png")
|
BOT_CORNER_BR_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_br.png")
|
||||||
|
# --- Additional Bot Bubble Types (Skins) ---
|
||||||
|
# Type 2
|
||||||
|
BOT_CORNER_TL_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tl_type2.png")
|
||||||
|
BOT_CORNER_BR_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_br_type2.png")
|
||||||
|
# Type 3
|
||||||
|
BOT_CORNER_TL_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tl_type3.png")
|
||||||
|
BOT_CORNER_BR_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_br_type3.png")
|
||||||
|
# --- End Additional Types ---
|
||||||
# Keywords
|
# Keywords
|
||||||
KEYWORD_wolf_LOWER_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower.png")
|
KEYWORD_wolf_LOWER_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower.png")
|
||||||
KEYWORD_Wolf_UPPER_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper.png")
|
KEYWORD_Wolf_UPPER_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper.png")
|
||||||
|
KEYWORD_wolf_LOWER_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower_type3.png") # Added for type3 bubbles
|
||||||
|
KEYWORD_Wolf_UPPER_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper_type3.png") # Added for type3 bubbles
|
||||||
# UI Elements
|
# UI Elements
|
||||||
COPY_MENU_ITEM_IMG = os.path.join(TEMPLATE_DIR, "copy_menu_item.png")
|
COPY_MENU_ITEM_IMG = os.path.join(TEMPLATE_DIR, "copy_menu_item.png")
|
||||||
PROFILE_OPTION_IMG = os.path.join(TEMPLATE_DIR, "profile_option.png")
|
PROFILE_OPTION_IMG = os.path.join(TEMPLATE_DIR, "profile_option.png")
|
||||||
@ -42,10 +64,38 @@ CHAT_INPUT_IMG = os.path.join(TEMPLATE_DIR, "chat_input.png")
|
|||||||
PROFILE_NAME_PAGE_IMG = os.path.join(TEMPLATE_DIR, "Profile_Name_page.png")
|
PROFILE_NAME_PAGE_IMG = os.path.join(TEMPLATE_DIR, "Profile_Name_page.png")
|
||||||
PROFILE_PAGE_IMG = os.path.join(TEMPLATE_DIR, "Profile_page.png")
|
PROFILE_PAGE_IMG = os.path.join(TEMPLATE_DIR, "Profile_page.png")
|
||||||
CHAT_ROOM_IMG = os.path.join(TEMPLATE_DIR, "chat_room.png")
|
CHAT_ROOM_IMG = os.path.join(TEMPLATE_DIR, "chat_room.png")
|
||||||
|
BASE_SCREEN_IMG = os.path.join(TEMPLATE_DIR, "base.png") # Added for navigation
|
||||||
|
WORLD_MAP_IMG = os.path.join(TEMPLATE_DIR, "World_map.png") # Added for navigation
|
||||||
# Add World/Private chat identifiers later
|
# Add World/Private chat identifiers later
|
||||||
WORLD_CHAT_IMG = os.path.join(TEMPLATE_DIR, "World_Label_normal.png") # Example
|
WORLD_CHAT_IMG = os.path.join(TEMPLATE_DIR, "World_Label_normal.png") # Example
|
||||||
PRIVATE_CHAT_IMG = os.path.join(TEMPLATE_DIR, "Private_Label_normal.png") # Example
|
PRIVATE_CHAT_IMG = os.path.join(TEMPLATE_DIR, "Private_Label_normal.png") # Example
|
||||||
|
|
||||||
|
# Position Icons (Near Bubble)
|
||||||
|
POS_DEV_IMG = os.path.join(TEMPLATE_DIR, "positions", "development.png")
|
||||||
|
POS_INT_IMG = os.path.join(TEMPLATE_DIR, "positions", "interior.png")
|
||||||
|
POS_SCI_IMG = os.path.join(TEMPLATE_DIR, "positions", "science.png")
|
||||||
|
POS_SEC_IMG = os.path.join(TEMPLATE_DIR, "positions", "security.png")
|
||||||
|
POS_STR_IMG = os.path.join(TEMPLATE_DIR, "positions", "strategy.png")
|
||||||
|
|
||||||
|
# Capitol Page Elements
|
||||||
|
CAPITOL_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "capitol", "capitol_#11.png")
|
||||||
|
PRESIDENT_TITLE_IMG = os.path.join(TEMPLATE_DIR, "capitol", "president_title.png")
|
||||||
|
POS_BTN_DEV_IMG = os.path.join(TEMPLATE_DIR, "capitol", "position_development.png")
|
||||||
|
POS_BTN_INT_IMG = os.path.join(TEMPLATE_DIR, "capitol", "position_interior.png")
|
||||||
|
POS_BTN_SCI_IMG = os.path.join(TEMPLATE_DIR, "capitol", "position_science.png")
|
||||||
|
POS_BTN_SEC_IMG = os.path.join(TEMPLATE_DIR, "capitol", "position_security.png")
|
||||||
|
POS_BTN_STR_IMG = os.path.join(TEMPLATE_DIR, "capitol", "position_strategy.png")
|
||||||
|
PAGE_DEV_IMG = os.path.join(TEMPLATE_DIR, "capitol", "page_DEVELOPMENT.png")
|
||||||
|
PAGE_INT_IMG = os.path.join(TEMPLATE_DIR, "capitol", "page_INTERIOR.png")
|
||||||
|
PAGE_SCI_IMG = os.path.join(TEMPLATE_DIR, "capitol", "page_SCIENCE.png")
|
||||||
|
PAGE_SEC_IMG = os.path.join(TEMPLATE_DIR, "capitol", "page_SECURITY.png")
|
||||||
|
PAGE_STR_IMG = os.path.join(TEMPLATE_DIR, "capitol", "page_STRATEGY.png")
|
||||||
|
DISMISS_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "capitol", "dismiss.png")
|
||||||
|
CONFIRM_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "capitol", "confirm.png")
|
||||||
|
CLOSE_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "capitol", "close_button.png")
|
||||||
|
BACK_ARROW_IMG = os.path.join(TEMPLATE_DIR, "capitol", "black_arrow_down.png")
|
||||||
|
|
||||||
|
|
||||||
# --- Operation Parameters (Consider moving to config.py) ---
|
# --- Operation Parameters (Consider moving to config.py) ---
|
||||||
CHAT_INPUT_REGION = None # Example: (100, 800, 500, 50)
|
CHAT_INPUT_REGION = None # Example: (100, 800, 500, 50)
|
||||||
CHAT_INPUT_CENTER_X = 400
|
CHAT_INPUT_CENTER_X = 400
|
||||||
@ -53,7 +103,7 @@ CHAT_INPUT_CENTER_Y = 1280
|
|||||||
SCREENSHOT_REGION = None
|
SCREENSHOT_REGION = None
|
||||||
CONFIDENCE_THRESHOLD = 0.8
|
CONFIDENCE_THRESHOLD = 0.8
|
||||||
STATE_CONFIDENCE_THRESHOLD = 0.7
|
STATE_CONFIDENCE_THRESHOLD = 0.7
|
||||||
AVATAR_OFFSET_X = -50
|
AVATAR_OFFSET_X = -55 # Adjusted as per user request (was -50)
|
||||||
BBOX_SIMILARITY_TOLERANCE = 10
|
BBOX_SIMILARITY_TOLERANCE = 10
|
||||||
RECENT_TEXT_HISTORY_MAXLEN = 5 # This state likely belongs in the coordinator
|
RECENT_TEXT_HISTORY_MAXLEN = 5 # This state likely belongs in the coordinator
|
||||||
|
|
||||||
@ -64,6 +114,7 @@ def are_bboxes_similar(bbox1: Optional[Tuple[int, int, int, int]],
|
|||||||
"""Check if two bounding boxes' top-left corners are close."""
|
"""Check if two bounding boxes' top-left corners are close."""
|
||||||
if bbox1 is None or bbox2 is None:
|
if bbox1 is None or bbox2 is None:
|
||||||
return False
|
return False
|
||||||
|
# Compare based on bbox top-left (index 0 and 1)
|
||||||
return abs(bbox1[0] - bbox2[0]) <= tolerance and abs(bbox1[1] - bbox2[1]) <= tolerance
|
return abs(bbox1[0] - bbox2[0]) <= tolerance and abs(bbox1[1] - bbox2[1]) <= tolerance
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@ -81,7 +132,7 @@ class DetectionModule:
|
|||||||
print("DetectionModule initialized.")
|
print("DetectionModule initialized.")
|
||||||
|
|
||||||
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]]:
|
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."""
|
"""Internal helper to find a template by its key. Returns list of CENTER coordinates."""
|
||||||
template_path = self.templates.get(template_key)
|
template_path = self.templates.get(template_key)
|
||||||
if not template_path:
|
if not template_path:
|
||||||
print(f"Error: Template key '{template_key}' not found in provided templates.")
|
print(f"Error: Template key '{template_key}' not found in provided templates.")
|
||||||
@ -99,9 +150,11 @@ class DetectionModule:
|
|||||||
current_confidence = confidence if confidence is not None else self.confidence
|
current_confidence = confidence if confidence is not None else self.confidence
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# locateAllOnScreen returns Box objects (left, top, width, height)
|
||||||
matches = pyautogui.locateAllOnScreen(template_path, region=current_region, confidence=current_confidence, grayscale=grayscale)
|
matches = pyautogui.locateAllOnScreen(template_path, region=current_region, confidence=current_confidence, grayscale=grayscale)
|
||||||
if matches:
|
if matches:
|
||||||
for box in matches:
|
for box in matches:
|
||||||
|
# Calculate center coordinates from the Box object
|
||||||
center_x = box.left + box.width // 2
|
center_x = box.left + box.width // 2
|
||||||
center_y = box.top + box.height // 2
|
center_y = box.top + box.height // 2
|
||||||
locations.append((center_x, center_y))
|
locations.append((center_x, center_y))
|
||||||
@ -111,88 +164,172 @@ class DetectionModule:
|
|||||||
print(f"Error finding template '{template_key}' ({template_path}): {e}")
|
print(f"Error finding template '{template_key}' ({template_path}): {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def _find_template_raw(self, template_key: str, confidence: Optional[float] = None, region: Optional[Tuple[int, int, int, int]] = None, grayscale: bool = False) -> List[Tuple[int, int, int, int]]:
|
||||||
|
"""Internal helper to find a template by its key. Returns list of raw Box tuples (left, top, width, height)."""
|
||||||
|
template_path = self.templates.get(template_key)
|
||||||
|
if not template_path:
|
||||||
|
print(f"Error: Template key '{template_key}' not found in provided templates.")
|
||||||
|
return []
|
||||||
|
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 []
|
||||||
|
|
||||||
|
locations = []
|
||||||
|
current_region = region if region is not None else self.region
|
||||||
|
current_confidence = confidence if confidence is not None else self.confidence
|
||||||
|
try:
|
||||||
|
# --- Temporary Debug Print ---
|
||||||
|
print(f"DEBUG: Searching for template '{template_key}' with confidence {current_confidence}...")
|
||||||
|
# --- End Temporary Debug Print ---
|
||||||
|
matches = pyautogui.locateAllOnScreen(template_path, region=current_region, confidence=current_confidence, grayscale=grayscale)
|
||||||
|
match_count = 0 # Initialize count
|
||||||
|
if matches:
|
||||||
|
for box in matches:
|
||||||
|
locations.append((box.left, box.top, box.width, box.height))
|
||||||
|
match_count += 1 # Increment count
|
||||||
|
# --- Temporary Debug Print ---
|
||||||
|
print(f"DEBUG: Found {match_count} instance(s) of template '{template_key}'.")
|
||||||
|
# --- End Temporary Debug Print ---
|
||||||
|
return locations
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error finding template raw '{template_key}' ({template_path}): {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
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]]]:
|
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."""
|
"""Find multiple templates by their keys. Returns center coordinates."""
|
||||||
results = {}
|
results = {}
|
||||||
for key in template_keys:
|
for key in template_keys:
|
||||||
results[key] = self._find_template(key, confidence=confidence, region=region)
|
results[key] = self._find_template(key, confidence=confidence, region=region)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def find_dialogue_bubbles(self) -> List[Tuple[Tuple[int, int, int, int], bool]]:
|
def find_dialogue_bubbles(self) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Scan screen for regular and bot bubble corners and pair them.
|
Scan screen for regular and multiple types of bot bubble corners and pair them.
|
||||||
Returns list of (bbox, is_bot_flag). Basic matching logic.
|
Returns a list of dictionaries, each containing:
|
||||||
|
{'bbox': (tl_x, tl_y, br_x, br_y), 'is_bot': bool, 'tl_coords': (original_tl_x, original_tl_y)}
|
||||||
"""
|
"""
|
||||||
all_bubbles_with_type = []
|
all_bubbles_info = []
|
||||||
|
processed_tls = set() # Keep track of TL corners already used in a bubble
|
||||||
|
|
||||||
# Find corners using the internal helper
|
# --- Find ALL Regular Bubble Corners (Raw Coordinates) ---
|
||||||
tl_corners = self._find_template('corner_tl')
|
regular_tl_keys = ['corner_tl', 'corner_tl_type2', 'corner_tl_type3'] # Modified
|
||||||
br_corners = self._find_template('corner_br')
|
regular_br_keys = ['corner_br', 'corner_br_type2', 'corner_br_type3'] # Modified
|
||||||
bot_tl_corners = self._find_template('bot_corner_tl')
|
|
||||||
bot_br_corners = self._find_template('bot_corner_br')
|
|
||||||
|
|
||||||
# Match regular bubbles
|
all_regular_tl_boxes = []
|
||||||
processed_tls = set()
|
for key in regular_tl_keys:
|
||||||
if tl_corners and br_corners:
|
all_regular_tl_boxes.extend(self._find_template_raw(key))
|
||||||
for i, tl in enumerate(tl_corners):
|
|
||||||
if i in processed_tls: continue
|
all_regular_br_boxes = []
|
||||||
potential_br = None
|
for key in regular_br_keys:
|
||||||
|
all_regular_br_boxes.extend(self._find_template_raw(key))
|
||||||
|
|
||||||
|
# --- Find Bot Bubble Corners (Raw Coordinates - Single Type) ---
|
||||||
|
bot_tl_boxes = self._find_template_raw('bot_corner_tl') # Modified
|
||||||
|
bot_br_boxes = self._find_template_raw('bot_corner_br') # Modified
|
||||||
|
|
||||||
|
# --- Match Regular Bubbles (Any Type TL with Any Type BR) ---
|
||||||
|
if all_regular_tl_boxes and all_regular_br_boxes:
|
||||||
|
for tl_box in all_regular_tl_boxes:
|
||||||
|
tl_coords = (tl_box[0], tl_box[1]) # Extract original TL (left, top)
|
||||||
|
# Skip if this TL is already part of a matched bubble
|
||||||
|
if tl_coords in processed_tls: continue
|
||||||
|
|
||||||
|
potential_br_box = None
|
||||||
min_dist_sq = float('inf')
|
min_dist_sq = float('inf')
|
||||||
for j, br in enumerate(br_corners):
|
# Find the closest valid BR corner (from any regular type) below and to the right
|
||||||
if br[0] > tl[0] + 20 and br[1] > tl[1] + 10:
|
for br_box in all_regular_br_boxes:
|
||||||
dist_sq = (br[0] - tl[0])**2 + (br[1] - tl[1])**2
|
br_coords = (br_box[0], br_box[1]) # BR top-left
|
||||||
|
if br_coords[0] > tl_coords[0] + 20 and br_coords[1] > tl_coords[1] + 10: # Basic geometric check
|
||||||
|
dist_sq = (br_coords[0] - tl_coords[0])**2 + (br_coords[1] - tl_coords[1])**2
|
||||||
if dist_sq < min_dist_sq:
|
if dist_sq < min_dist_sq:
|
||||||
potential_br = br
|
potential_br_box = br_box
|
||||||
min_dist_sq = dist_sq
|
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, False))
|
|
||||||
processed_tls.add(i)
|
|
||||||
|
|
||||||
# Match Bot bubbles
|
if potential_br_box:
|
||||||
processed_bot_tls = set()
|
# Calculate bbox using TL's top-left and BR's bottom-right
|
||||||
if bot_tl_corners and bot_br_corners:
|
bubble_bbox = (tl_coords[0], tl_coords[1],
|
||||||
for i, tl in enumerate(bot_tl_corners):
|
potential_br_box[0] + potential_br_box[2], potential_br_box[1] + potential_br_box[3])
|
||||||
if i in processed_bot_tls: continue
|
all_bubbles_info.append({
|
||||||
potential_br = None
|
'bbox': bubble_bbox,
|
||||||
|
'is_bot': False,
|
||||||
|
'tl_coords': tl_coords # Store the original TL coords
|
||||||
|
})
|
||||||
|
processed_tls.add(tl_coords) # Mark this TL as used
|
||||||
|
|
||||||
|
# --- Match Bot Bubbles (Single Type) ---
|
||||||
|
if bot_tl_boxes and bot_br_boxes:
|
||||||
|
for tl_box in bot_tl_boxes:
|
||||||
|
tl_coords = (tl_box[0], tl_box[1]) # Extract original TL (left, top)
|
||||||
|
# Skip if this TL is already part of a matched bubble
|
||||||
|
if tl_coords in processed_tls: continue
|
||||||
|
|
||||||
|
potential_br_box = None
|
||||||
min_dist_sq = float('inf')
|
min_dist_sq = float('inf')
|
||||||
for j, br in enumerate(bot_br_corners):
|
# Find the closest valid BR corner below and to the right
|
||||||
if br[0] > tl[0] + 20 and br[1] > tl[1] + 10:
|
for br_box in bot_br_boxes:
|
||||||
dist_sq = (br[0] - tl[0])**2 + (br[1] - tl[1])**2
|
br_coords = (br_box[0], br_box[1]) # BR top-left
|
||||||
|
if br_coords[0] > tl_coords[0] + 20 and br_coords[1] > tl_coords[1] + 10: # Basic geometric check
|
||||||
|
dist_sq = (br_coords[0] - tl_coords[0])**2 + (br_coords[1] - tl_coords[1])**2
|
||||||
if dist_sq < min_dist_sq:
|
if dist_sq < min_dist_sq:
|
||||||
potential_br = br
|
potential_br_box = br_box
|
||||||
min_dist_sq = dist_sq
|
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
|
if potential_br_box:
|
||||||
|
# Calculate bbox using TL's top-left and BR's bottom-right
|
||||||
|
bubble_bbox = (tl_coords[0], tl_coords[1],
|
||||||
|
potential_br_box[0] + potential_br_box[2], potential_br_box[1] + potential_br_box[3])
|
||||||
|
all_bubbles_info.append({
|
||||||
|
'bbox': bubble_bbox,
|
||||||
|
'is_bot': True,
|
||||||
|
'tl_coords': tl_coords # Store the original TL coords
|
||||||
|
})
|
||||||
|
processed_tls.add(tl_coords) # Mark this TL as used
|
||||||
|
|
||||||
|
# Note: This logic prioritizes matching regular bubbles first, then bot bubbles.
|
||||||
|
# Confidence thresholds might need tuning.
|
||||||
|
return all_bubbles_info
|
||||||
|
|
||||||
def find_keyword_in_region(self, region: Tuple[int, int, int, int]) -> Optional[Tuple[int, int]]:
|
def find_keyword_in_region(self, region: Tuple[int, int, int, int]) -> Optional[Tuple[int, int]]:
|
||||||
"""Look for keywords within a specified region."""
|
"""Look for keywords within a specified region. Returns center coordinates."""
|
||||||
if region[2] <= 0 or region[3] <= 0: return None # Invalid region width/height
|
if region[2] <= 0 or region[3] <= 0: return None # Invalid region width/height
|
||||||
|
|
||||||
# Try lowercase
|
# Try original lowercase with grayscale matching
|
||||||
locations_lower = self._find_template('keyword_wolf_lower', region=region)
|
locations_lower = self._find_template('keyword_wolf_lower', region=region, grayscale=True)
|
||||||
if locations_lower:
|
if locations_lower:
|
||||||
print(f"Found keyword (lowercase) in region {region}, position: {locations_lower[0]}")
|
print(f"Found keyword (lowercase, grayscale) in region {region}, position: {locations_lower[0]}")
|
||||||
return locations_lower[0]
|
return locations_lower[0]
|
||||||
|
|
||||||
# Try uppercase
|
# Try original uppercase with grayscale matching
|
||||||
locations_upper = self._find_template('keyword_wolf_upper', region=region)
|
locations_upper = self._find_template('keyword_wolf_upper', region=region, grayscale=True)
|
||||||
if locations_upper:
|
if locations_upper:
|
||||||
print(f"Found keyword (uppercase) in region {region}, position: {locations_upper[0]}")
|
print(f"Found keyword (uppercase, grayscale) in region {region}, position: {locations_upper[0]}")
|
||||||
return locations_upper[0]
|
return locations_upper[0]
|
||||||
|
|
||||||
|
# Try type3 lowercase (white text, no grayscale)
|
||||||
|
locations_lower_type3 = self._find_template('keyword_wolf_lower_type3', region=region, grayscale=False) # Added type3 check
|
||||||
|
if locations_lower_type3:
|
||||||
|
print(f"Found keyword (lowercase, type3) in region {region}, position: {locations_lower_type3[0]}")
|
||||||
|
return locations_lower_type3[0]
|
||||||
|
|
||||||
|
# Try type3 uppercase (white text, no grayscale)
|
||||||
|
locations_upper_type3 = self._find_template('keyword_wolf_upper_type3', region=region, grayscale=False) # Added type3 check
|
||||||
|
if locations_upper_type3:
|
||||||
|
print(f"Found keyword (uppercase, type3) in region {region}, position: {locations_upper_type3[0]}")
|
||||||
|
return locations_upper_type3[0]
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def calculate_avatar_coords(self, bubble_bbox: Tuple[int, int, int, int], offset_x: int = AVATAR_OFFSET_X) -> Tuple[int, int]:
|
def calculate_avatar_coords(self, bubble_tl_coords: Tuple[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]
|
Calculate avatar coordinates based on the EXACT top-left corner coordinates of the bubble.
|
||||||
|
Uses the Y-coordinate of the TL corner directly.
|
||||||
|
"""
|
||||||
|
tl_x, tl_y = bubble_tl_coords[0], bubble_tl_coords[1]
|
||||||
avatar_x = tl_x + offset_x
|
avatar_x = tl_x + offset_x
|
||||||
avatar_y = tl_y # Assuming Y is same as top-left
|
avatar_y = tl_y # Use the exact Y from the detected TL corner
|
||||||
# print(f"Calculated avatar coordinates: ({int(avatar_x)}, {int(avatar_y)})") # Reduce noise
|
# print(f"Calculated avatar coordinates using TL {bubble_tl_coords}: ({int(avatar_x)}, {int(avatar_y)})") # Reduce noise
|
||||||
return (int(avatar_x), int(avatar_y))
|
return (int(avatar_x), int(avatar_y))
|
||||||
|
|
||||||
def get_current_ui_state(self) -> str:
|
def get_current_ui_state(self) -> str:
|
||||||
@ -277,7 +414,7 @@ class InteractionModule:
|
|||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
self.click_at(coords[0], coords[1])
|
self.click_at(coords[0], coords[1])
|
||||||
time.sleep(0.2) # Wait for menu/reaction
|
time.sleep(0.1) # Wait for menu/reaction
|
||||||
|
|
||||||
copied = False
|
copied = False
|
||||||
# Try finding "Copy" menu item first
|
# Try finding "Copy" menu item first
|
||||||
@ -292,7 +429,7 @@ class InteractionModule:
|
|||||||
print("'Copy' menu item not found. Attempting Ctrl+C.")
|
print("'Copy' menu item not found. Attempting Ctrl+C.")
|
||||||
try:
|
try:
|
||||||
self.hotkey('ctrl', 'c')
|
self.hotkey('ctrl', 'c')
|
||||||
time.sleep(0.2)
|
time.sleep(0.1)
|
||||||
print("Simulated Ctrl+C.")
|
print("Simulated Ctrl+C.")
|
||||||
copied = True
|
copied = True
|
||||||
except Exception as e_ctrlc:
|
except Exception as e_ctrlc:
|
||||||
@ -323,7 +460,7 @@ class InteractionModule:
|
|||||||
try:
|
try:
|
||||||
# 1. Click avatar
|
# 1. Click avatar
|
||||||
self.click_at(avatar_coords[0], avatar_coords[1])
|
self.click_at(avatar_coords[0], avatar_coords[1])
|
||||||
time.sleep(0.3) # Wait for profile card
|
time.sleep(0.1) # Wait for profile card
|
||||||
|
|
||||||
# 2. Find and click profile option
|
# 2. Find and click profile option
|
||||||
profile_option_locations = self.detector._find_template('profile_option', confidence=0.7)
|
profile_option_locations = self.detector._find_template('profile_option', confidence=0.7)
|
||||||
@ -332,7 +469,7 @@ class InteractionModule:
|
|||||||
return None # Fail early if critical step missing
|
return None # Fail early if critical step missing
|
||||||
self.click_at(profile_option_locations[0][0], profile_option_locations[0][1])
|
self.click_at(profile_option_locations[0][0], profile_option_locations[0][1])
|
||||||
print("Clicked user details option.")
|
print("Clicked user details option.")
|
||||||
time.sleep(0.3) # Wait for user details window
|
time.sleep(0.1) # Wait for user details window
|
||||||
|
|
||||||
# 3. Find and click "Copy Name" button
|
# 3. Find and click "Copy Name" button
|
||||||
copy_name_locations = self.detector._find_template('copy_name_button', confidence=0.7)
|
copy_name_locations = self.detector._find_template('copy_name_button', confidence=0.7)
|
||||||
@ -385,14 +522,14 @@ class InteractionModule:
|
|||||||
|
|
||||||
# Click input, paste, send
|
# Click input, paste, send
|
||||||
self.click_at(input_coords[0], input_coords[1])
|
self.click_at(input_coords[0], input_coords[1])
|
||||||
time.sleep(0.3)
|
time.sleep(0.1)
|
||||||
|
|
||||||
print("Pasting response...")
|
print("Pasting response...")
|
||||||
self.set_clipboard(reply_text)
|
self.set_clipboard(reply_text)
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
try:
|
try:
|
||||||
self.hotkey('ctrl', 'v')
|
self.hotkey('ctrl', 'v')
|
||||||
time.sleep(0.5)
|
time.sleep(0.1)
|
||||||
print("Pasted.")
|
print("Pasted.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error pasting response: {e}")
|
print(f"Error pasting response: {e}")
|
||||||
@ -412,12 +549,175 @@ class InteractionModule:
|
|||||||
try:
|
try:
|
||||||
self.press_key('enter')
|
self.press_key('enter')
|
||||||
print("Pressed Enter.")
|
print("Pressed Enter.")
|
||||||
time.sleep(0.5)
|
time.sleep(0.1)
|
||||||
return True
|
return True
|
||||||
except Exception as e_enter:
|
except Exception as e_enter:
|
||||||
print(f"Error pressing Enter: {e_enter}")
|
print(f"Error pressing Enter: {e_enter}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Position Removal Logic
|
||||||
|
# ==============================================================================
|
||||||
|
def remove_user_position(detector: DetectionModule, interactor: InteractionModule, trigger_bubble_region: Tuple[int, int, int, int]) -> bool:
|
||||||
|
"""
|
||||||
|
Performs the sequence of UI actions to remove a user's position based on the triggering chat bubble.
|
||||||
|
Returns True if successful, False otherwise.
|
||||||
|
"""
|
||||||
|
print(f"\n--- Starting Position Removal Process (Trigger Bubble Region: {trigger_bubble_region}) ---")
|
||||||
|
bubble_x, bubble_y, bubble_w, bubble_h = trigger_bubble_region # This is the BBOX, y is top
|
||||||
|
|
||||||
|
# 1. Find the closest position icon above the bubble
|
||||||
|
search_region_y_end = bubble_y
|
||||||
|
search_region_y_start = max(0, bubble_y - 55) # Search 55 pixels above
|
||||||
|
search_region_x_start = max(0, bubble_x - 100) # Search wider horizontally
|
||||||
|
search_region_x_end = bubble_x + bubble_w + 100
|
||||||
|
search_region = (search_region_x_start, search_region_y_start, search_region_x_end - search_region_x_start, search_region_y_end - search_region_y_start)
|
||||||
|
print(f"Searching for position icons in region: {search_region}")
|
||||||
|
|
||||||
|
position_templates = {
|
||||||
|
'DEVELOPMENT': POS_DEV_IMG, 'INTERIOR': POS_INT_IMG, 'SCIENCE': POS_SCI_IMG,
|
||||||
|
'SECURITY': POS_SEC_IMG, 'STRATEGY': POS_STR_IMG
|
||||||
|
}
|
||||||
|
found_positions = []
|
||||||
|
for name, path in position_templates.items():
|
||||||
|
# Use unique keys for detector templates
|
||||||
|
locations = detector._find_template(name.lower() + '_pos', confidence=0.75, region=search_region)
|
||||||
|
for loc in locations:
|
||||||
|
found_positions.append({'name': name, 'coords': loc, 'path': path})
|
||||||
|
|
||||||
|
if not found_positions:
|
||||||
|
print("Error: No position icons found near the trigger bubble.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Find the closest one to the bubble's top-center
|
||||||
|
bubble_top_center_x = bubble_x + bubble_w // 2
|
||||||
|
bubble_top_center_y = bubble_y
|
||||||
|
closest_position = min(found_positions, key=lambda p:
|
||||||
|
(p['coords'][0] - bubble_top_center_x)**2 + (p['coords'][1] - bubble_top_center_y)**2)
|
||||||
|
|
||||||
|
target_position_name = closest_position['name']
|
||||||
|
print(f"Found pending position: |{target_position_name}| at {closest_position['coords']}")
|
||||||
|
|
||||||
|
# 2. Click user avatar (offset from bubble top-left)
|
||||||
|
# IMPORTANT: Use the bubble_y (top of the bbox) for the click Y coordinate.
|
||||||
|
# The AVATAR_OFFSET_X handles the horizontal positioning relative to the bubble's left edge (bubble_x).
|
||||||
|
avatar_click_x = bubble_x + AVATAR_OFFSET_X # Use constant offset
|
||||||
|
avatar_click_y = bubble_y # Use the top Y coordinate of the bubble's bounding box
|
||||||
|
print(f"Clicking avatar at estimated position: ({avatar_click_x}, {avatar_click_y}) based on bubble top-left ({bubble_x}, {bubble_y})")
|
||||||
|
interactor.click_at(avatar_click_x, avatar_click_y)
|
||||||
|
time.sleep(0.15) # Wait for profile page
|
||||||
|
|
||||||
|
# 3. Verify Profile Page and Click Capitol Button
|
||||||
|
if not detector._find_template('profile_page', confidence=detector.state_confidence):
|
||||||
|
print("Error: Failed to verify Profile Page after clicking avatar.")
|
||||||
|
perform_state_cleanup(detector, interactor) # Attempt cleanup
|
||||||
|
return False
|
||||||
|
print("Profile page verified.")
|
||||||
|
|
||||||
|
capitol_button_locs = detector._find_template('capitol_button', confidence=0.8)
|
||||||
|
if not capitol_button_locs:
|
||||||
|
print("Error: Capitol button (#11) not found on profile page.")
|
||||||
|
perform_state_cleanup(detector, interactor)
|
||||||
|
return False
|
||||||
|
interactor.click_at(capitol_button_locs[0][0], capitol_button_locs[0][1])
|
||||||
|
print("Clicked Capitol button.")
|
||||||
|
time.sleep(0.15) # Wait for capitol page
|
||||||
|
|
||||||
|
# 4. Verify Capitol Page
|
||||||
|
if not detector._find_template('president_title', confidence=detector.state_confidence):
|
||||||
|
print("Error: Failed to verify Capitol Page (President Title not found).")
|
||||||
|
perform_state_cleanup(detector, interactor)
|
||||||
|
return False
|
||||||
|
print("Capitol page verified.")
|
||||||
|
|
||||||
|
# 5. Find and Click Corresponding Position Button
|
||||||
|
position_button_templates = {
|
||||||
|
'DEVELOPMENT': 'pos_btn_dev', 'INTERIOR': 'pos_btn_int', 'SCIENCE': 'pos_btn_sci',
|
||||||
|
'SECURITY': 'pos_btn_sec', 'STRATEGY': 'pos_btn_str'
|
||||||
|
}
|
||||||
|
target_button_key = position_button_templates.get(target_position_name)
|
||||||
|
if not target_button_key:
|
||||||
|
print(f"Error: Internal error - unknown position name '{target_position_name}'")
|
||||||
|
perform_state_cleanup(detector, interactor)
|
||||||
|
return False
|
||||||
|
|
||||||
|
pos_button_locs = detector._find_template(target_button_key, confidence=0.8)
|
||||||
|
if not pos_button_locs:
|
||||||
|
print(f"Error: Position button for '{target_position_name}' not found on Capitol page.")
|
||||||
|
perform_state_cleanup(detector, interactor)
|
||||||
|
return False
|
||||||
|
interactor.click_at(pos_button_locs[0][0], pos_button_locs[0][1])
|
||||||
|
print(f"Clicked '{target_position_name}' position button.")
|
||||||
|
time.sleep(0.15) # Wait for position page
|
||||||
|
|
||||||
|
# 6. Verify Position Page
|
||||||
|
position_page_templates = {
|
||||||
|
'DEVELOPMENT': 'page_dev', 'INTERIOR': 'page_int', 'SCIENCE': 'page_sci',
|
||||||
|
'SECURITY': 'page_sec', 'STRATEGY': 'page_str'
|
||||||
|
}
|
||||||
|
target_page_key = position_page_templates.get(target_position_name)
|
||||||
|
if not target_page_key:
|
||||||
|
print(f"Error: Internal error - unknown position name '{target_position_name}' for page verification")
|
||||||
|
perform_state_cleanup(detector, interactor)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not detector._find_template(target_page_key, confidence=detector.state_confidence):
|
||||||
|
print(f"Error: Failed to verify correct position page for '{target_position_name}'.")
|
||||||
|
perform_state_cleanup(detector, interactor)
|
||||||
|
return False
|
||||||
|
print(f"Verified '{target_position_name}' position page.")
|
||||||
|
|
||||||
|
# 7. Find and Click Dismiss Button
|
||||||
|
dismiss_locs = detector._find_template('dismiss_button', confidence=0.8)
|
||||||
|
if not dismiss_locs:
|
||||||
|
print("Error: Dismiss button not found on position page.")
|
||||||
|
perform_state_cleanup(detector, interactor)
|
||||||
|
return False
|
||||||
|
interactor.click_at(dismiss_locs[0][0], dismiss_locs[0][1])
|
||||||
|
print("Clicked Dismiss button.")
|
||||||
|
time.sleep(0.1) # Wait for confirmation
|
||||||
|
|
||||||
|
# 8. Find and Click Confirm Button
|
||||||
|
confirm_locs = detector._find_template('confirm_button', confidence=0.8)
|
||||||
|
if not confirm_locs:
|
||||||
|
print("Error: Confirm button not found after clicking dismiss.")
|
||||||
|
# Don't cleanup here, might be stuck in confirmation state
|
||||||
|
return False # Indicate failure, but let main loop decide next step
|
||||||
|
interactor.click_at(confirm_locs[0][0], confirm_locs[0][1])
|
||||||
|
print("Clicked Confirm button. Position should be dismissed.")
|
||||||
|
time.sleep(0.1) # Wait for action to complete
|
||||||
|
|
||||||
|
# 9. Cleanup: Return to Chat Room
|
||||||
|
# Click Close on position page (should now be back on capitol page implicitly)
|
||||||
|
close_locs = detector._find_template('close_button', confidence=0.8)
|
||||||
|
if close_locs:
|
||||||
|
interactor.click_at(close_locs[0][0], close_locs[0][1])
|
||||||
|
print("Clicked Close button (returning to Capitol).")
|
||||||
|
time.sleep(0.15)
|
||||||
|
else:
|
||||||
|
print("Warning: Close button not found after confirm, attempting back arrow anyway.")
|
||||||
|
|
||||||
|
# Click Back Arrow on Capitol page (should return to profile)
|
||||||
|
back_arrow_locs = detector._find_template('back_arrow', confidence=0.8)
|
||||||
|
if back_arrow_locs:
|
||||||
|
interactor.click_at(back_arrow_locs[0][0], back_arrow_locs[0][1])
|
||||||
|
print("Clicked Back Arrow (returning to Profile).")
|
||||||
|
time.sleep(0.15)
|
||||||
|
else:
|
||||||
|
print("Warning: Back arrow not found on Capitol page, attempting ESC cleanup.")
|
||||||
|
|
||||||
|
# Use standard ESC cleanup
|
||||||
|
print("Initiating final ESC cleanup to return to chat...")
|
||||||
|
cleanup_success = perform_state_cleanup(detector, interactor)
|
||||||
|
|
||||||
|
if cleanup_success:
|
||||||
|
print("--- Position Removal Process Completed Successfully ---")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("--- Position Removal Process Completed, but failed to confirm return to chat room ---")
|
||||||
|
return False # Technically removed, but UI state uncertain
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Coordinator Logic (Placeholder - To be implemented in main.py)
|
# Coordinator Logic (Placeholder - To be implemented in main.py)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@ -432,7 +732,7 @@ def perform_state_cleanup(detector: DetectionModule, interactor: InteractionModu
|
|||||||
returned_to_chat = False
|
returned_to_chat = False
|
||||||
for attempt in range(max_attempts):
|
for attempt in range(max_attempts):
|
||||||
print(f"Cleanup attempt #{attempt + 1}/{max_attempts}")
|
print(f"Cleanup attempt #{attempt + 1}/{max_attempts}")
|
||||||
time.sleep(0.2)
|
time.sleep(0.1)
|
||||||
|
|
||||||
current_state = detector.get_current_ui_state()
|
current_state = detector.get_current_ui_state()
|
||||||
print(f"Detected state: {current_state}")
|
print(f"Detected state: {current_state}")
|
||||||
@ -444,14 +744,14 @@ def perform_state_cleanup(detector: DetectionModule, interactor: InteractionModu
|
|||||||
elif current_state == 'user_details' or current_state == 'profile_card':
|
elif current_state == 'user_details' or current_state == 'profile_card':
|
||||||
print(f"{current_state.replace('_', ' ').title()} detected, pressing ESC...")
|
print(f"{current_state.replace('_', ' ').title()} detected, pressing ESC...")
|
||||||
interactor.press_key('esc')
|
interactor.press_key('esc')
|
||||||
time.sleep(0.3) # Wait longer for UI response after ESC
|
time.sleep(0.1) # Wait longer for UI response after ESC
|
||||||
continue
|
continue
|
||||||
else: # Unknown state
|
else: # Unknown state
|
||||||
print("Unknown page state detected.")
|
print("Unknown page state detected.")
|
||||||
if attempt < max_attempts - 1:
|
if attempt < max_attempts - 1:
|
||||||
print("Trying one ESC press as fallback...")
|
print("Trying one ESC press as fallback...")
|
||||||
interactor.press_key('esc')
|
interactor.press_key('esc')
|
||||||
time.sleep(0.3)
|
time.sleep(0.1)
|
||||||
else:
|
else:
|
||||||
print("Maximum attempts reached, stopping cleanup.")
|
print("Maximum attempts reached, stopping cleanup.")
|
||||||
break
|
break
|
||||||
@ -473,26 +773,64 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu
|
|||||||
# Load templates directly using constants defined in this file for now
|
# Load templates directly using constants defined in this file for now
|
||||||
# Consider passing config or a template loader object in the future
|
# Consider passing config or a template loader object in the future
|
||||||
templates = {
|
templates = {
|
||||||
|
# Regular Bubble (Original + Skins) - Keys match those used in find_dialogue_bubbles
|
||||||
'corner_tl': CORNER_TL_IMG, 'corner_br': CORNER_BR_IMG,
|
'corner_tl': CORNER_TL_IMG, 'corner_br': CORNER_BR_IMG,
|
||||||
|
'corner_tl_type2': CORNER_TL_TYPE2_IMG, 'corner_br_type2': CORNER_BR_TYPE2_IMG,
|
||||||
|
'corner_tl_type3': CORNER_TL_TYPE3_IMG, 'corner_br_type3': CORNER_BR_TYPE3_IMG, # Corrected: Added missing keys here
|
||||||
|
# Bot Bubble (Single Type)
|
||||||
'bot_corner_tl': BOT_CORNER_TL_IMG, 'bot_corner_br': BOT_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,
|
# Keywords & UI Elements
|
||||||
|
'keyword_wolf_lower': KEYWORD_wolf_LOWER_IMG,
|
||||||
|
'keyword_wolf_upper': KEYWORD_Wolf_UPPER_IMG,
|
||||||
|
'keyword_wolf_lower_type3': KEYWORD_wolf_LOWER_TYPE3_IMG, # Added
|
||||||
|
'keyword_wolf_upper_type3': KEYWORD_Wolf_UPPER_TYPE3_IMG, # Added
|
||||||
'copy_menu_item': COPY_MENU_ITEM_IMG, 'profile_option': PROFILE_OPTION_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,
|
'copy_name_button': COPY_NAME_BUTTON_IMG, 'send_button': SEND_BUTTON_IMG,
|
||||||
'chat_input': CHAT_INPUT_IMG, 'profile_name_page': PROFILE_NAME_PAGE_IMG,
|
'chat_input': CHAT_INPUT_IMG, 'profile_name_page': PROFILE_NAME_PAGE_IMG,
|
||||||
'profile_page': PROFILE_PAGE_IMG, 'chat_room': CHAT_ROOM_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
|
'base_screen': BASE_SCREEN_IMG, 'world_map_screen': WORLD_MAP_IMG, # Added for navigation
|
||||||
|
'world_chat': WORLD_CHAT_IMG, 'private_chat': PRIVATE_CHAT_IMG,
|
||||||
|
# Add position templates
|
||||||
|
'development_pos': POS_DEV_IMG, 'interior_pos': POS_INT_IMG, 'science_pos': POS_SCI_IMG,
|
||||||
|
'security_pos': POS_SEC_IMG, 'strategy_pos': POS_STR_IMG,
|
||||||
|
# Add capitol templates
|
||||||
|
'capitol_button': CAPITOL_BUTTON_IMG, 'president_title': PRESIDENT_TITLE_IMG,
|
||||||
|
'pos_btn_dev': POS_BTN_DEV_IMG, 'pos_btn_int': POS_BTN_INT_IMG, 'pos_btn_sci': POS_BTN_SCI_IMG,
|
||||||
|
'pos_btn_sec': POS_BTN_SEC_IMG, 'pos_btn_str': POS_BTN_STR_IMG,
|
||||||
|
'page_dev': PAGE_DEV_IMG, 'page_int': PAGE_INT_IMG, 'page_sci': PAGE_SCI_IMG,
|
||||||
|
'page_sec': PAGE_SEC_IMG, 'page_str': PAGE_STR_IMG,
|
||||||
|
'dismiss_button': DISMISS_BUTTON_IMG, 'confirm_button': CONFIRM_BUTTON_IMG,
|
||||||
|
'close_button': CLOSE_BUTTON_IMG, 'back_arrow': BACK_ARROW_IMG
|
||||||
}
|
}
|
||||||
# Use default confidence/region settings from constants
|
# Use default confidence/region settings from constants
|
||||||
detector = DetectionModule(templates, confidence=CONFIDENCE_THRESHOLD, state_confidence=STATE_CONFIDENCE_THRESHOLD, region=SCREENSHOT_REGION)
|
detector = DetectionModule(templates, confidence=CONFIDENCE_THRESHOLD, state_confidence=STATE_CONFIDENCE_THRESHOLD, region=SCREENSHOT_REGION)
|
||||||
# Use default input coords/keys from constants
|
# 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')
|
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) ---
|
# --- State Management (Local to this monitoring thread) ---
|
||||||
last_processed_bubble_bbox = None
|
last_processed_bubble_info = None # Store the whole dict now
|
||||||
recent_texts = collections.deque(maxlen=RECENT_TEXT_HISTORY_MAXLEN) # Context-specific history needed
|
recent_texts = collections.deque(maxlen=RECENT_TEXT_HISTORY_MAXLEN) # Context-specific history needed
|
||||||
|
screenshot_counter = 0 # Initialize counter for debug screenshots
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# --- Process Commands First (Non-blocking) ---
|
# --- Check for Main Screen Navigation First ---
|
||||||
|
try:
|
||||||
|
base_locs = detector._find_template('base_screen', confidence=0.8)
|
||||||
|
map_locs = detector._find_template('world_map_screen', confidence=0.8)
|
||||||
|
if base_locs or map_locs:
|
||||||
|
print("UI Thread: Detected main screen (Base or World Map). Clicking to return to chat...")
|
||||||
|
# Coordinates provided by user (adjust if needed based on actual screen resolution/layout)
|
||||||
|
# IMPORTANT: Ensure these coordinates are correct for the target window/resolution
|
||||||
|
target_x, target_y = 600, 1300
|
||||||
|
interactor.click_at(target_x, target_y)
|
||||||
|
time.sleep(0.2) # Short delay after click
|
||||||
|
print("UI Thread: Clicked to return to chat. Re-checking screen state...")
|
||||||
|
continue # Skip the rest of the loop and re-evaluate
|
||||||
|
except Exception as nav_err:
|
||||||
|
print(f"UI Thread: Error during main screen navigation check: {nav_err}")
|
||||||
|
# Decide if you want to continue or pause after error
|
||||||
|
|
||||||
|
# --- Process Commands Second (Non-blocking) ---
|
||||||
try:
|
try:
|
||||||
command_data = command_queue.get_nowait() # Check for commands without blocking
|
command_data = command_queue.get_nowait() # Check for commands without blocking
|
||||||
action = command_data.get('action')
|
action = command_data.get('action')
|
||||||
@ -503,6 +841,16 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu
|
|||||||
interactor.send_chat_message(text_to_send)
|
interactor.send_chat_message(text_to_send)
|
||||||
else:
|
else:
|
||||||
print("UI Thread: Received send_reply command with no text.")
|
print("UI Thread: Received send_reply command with no text.")
|
||||||
|
elif action == 'remove_position': # <--- Handle new command
|
||||||
|
region = command_data.get('trigger_bubble_region')
|
||||||
|
if region:
|
||||||
|
print(f"UI Thread: Received command to remove position triggered by bubble region: {region}")
|
||||||
|
# Call the new UI function
|
||||||
|
success = remove_user_position(detector, interactor, region) # Call synchronous function
|
||||||
|
print(f"UI Thread: Position removal attempt finished. Success: {success}")
|
||||||
|
# Note: No need to send result back unless main thread needs confirmation
|
||||||
|
else:
|
||||||
|
print("UI Thread: Received remove_position command without trigger_bubble_region.")
|
||||||
else:
|
else:
|
||||||
print(f"UI Thread: Received unknown command: {action}")
|
print(f"UI Thread: Received unknown command: {action}")
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
@ -510,49 +858,100 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu
|
|||||||
except Exception as cmd_err:
|
except Exception as cmd_err:
|
||||||
print(f"UI Thread: Error processing command queue: {cmd_err}")
|
print(f"UI Thread: Error processing command queue: {cmd_err}")
|
||||||
|
|
||||||
# --- Then Perform UI Monitoring ---
|
# --- Verify Chat Room State Before Bubble Detection ---
|
||||||
|
try:
|
||||||
|
# Use a slightly lower confidence maybe, or state_confidence
|
||||||
|
chat_room_locs = detector._find_template('chat_room', confidence=detector.state_confidence)
|
||||||
|
if not chat_room_locs:
|
||||||
|
print("UI Thread: Not in chat room state before bubble detection. Attempting cleanup...")
|
||||||
|
# Call the existing cleanup function to try and return
|
||||||
|
perform_state_cleanup(detector, interactor)
|
||||||
|
# Regardless of cleanup success, restart the loop to re-evaluate state from the top
|
||||||
|
print("UI Thread: Continuing loop after attempting chat room cleanup.")
|
||||||
|
time.sleep(0.5) # Small pause after cleanup attempt
|
||||||
|
continue
|
||||||
|
# else: # Optional: Log if chat room is confirmed
|
||||||
|
# print("UI Thread: Chat room state confirmed.")
|
||||||
|
|
||||||
|
except Exception as state_check_err:
|
||||||
|
print(f"UI Thread: Error checking for chat room state: {state_check_err}")
|
||||||
|
# Decide how to handle error - maybe pause and retry? For now, continue cautiously.
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Then Perform UI Monitoring (Bubble Detection) ---
|
||||||
try:
|
try:
|
||||||
# 1. Detect Bubbles
|
# 1. Detect Bubbles
|
||||||
all_bubbles = detector.find_dialogue_bubbles()
|
all_bubbles_data = detector.find_dialogue_bubbles() # Returns list of dicts
|
||||||
if not all_bubbles: time.sleep(2); continue
|
if not all_bubbles_data: time.sleep(2); continue
|
||||||
|
|
||||||
# Filter out bot bubbles, find newest non-bot bubble (example logic)
|
# 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]
|
other_bubbles_data = [b_info for b_info in all_bubbles_data if not b_info['is_bot']]
|
||||||
if not other_bubbles: time.sleep(2); continue
|
if not other_bubbles_data: time.sleep(0.2); continue
|
||||||
# Simple logic: assume lowest bubble is newest (might need improvement)
|
# 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
|
# Sort by bbox bottom y-coordinate (index 3)
|
||||||
|
target_bubble_info = max(other_bubbles_data, key=lambda b_info: b_info['bbox'][3])
|
||||||
|
|
||||||
# 2. Check for Duplicates (Position & Content)
|
# 2. Check for Duplicates (Position & Content)
|
||||||
if are_bboxes_similar(target_bubble, last_processed_bubble_bbox):
|
# Compare using the 'bbox' from the info dicts
|
||||||
time.sleep(2); continue
|
if are_bboxes_similar(target_bubble_info.get('bbox'), last_processed_bubble_info.get('bbox') if last_processed_bubble_info else None):
|
||||||
|
time.sleep(0.2); continue
|
||||||
|
|
||||||
# 3. Detect Keyword in Bubble
|
# 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])
|
target_bbox = target_bubble_info['bbox']
|
||||||
|
bubble_region = (target_bbox[0], target_bbox[1], target_bbox[2]-target_bbox[0], target_bbox[3]-target_bbox[1])
|
||||||
keyword_coords = detector.find_keyword_in_region(bubble_region)
|
keyword_coords = detector.find_keyword_in_region(bubble_region)
|
||||||
|
|
||||||
if keyword_coords:
|
if keyword_coords:
|
||||||
print(f"\n!!! Keyword detected in bubble {target_bubble} !!!")
|
print(f"\n!!! Keyword detected in bubble {target_bbox} !!!")
|
||||||
|
|
||||||
|
# --- Debug Screenshot Logic ---
|
||||||
|
try:
|
||||||
|
screenshot_index = (screenshot_counter % MAX_DEBUG_SCREENSHOTS) + 1
|
||||||
|
screenshot_filename = f"debug_bubble_{screenshot_index}.png"
|
||||||
|
screenshot_path = os.path.join(DEBUG_SCREENSHOT_DIR, screenshot_filename)
|
||||||
|
# --- Enhanced Logging ---
|
||||||
|
print(f"Attempting to save debug screenshot to: {screenshot_path}")
|
||||||
|
print(f"Screenshot Region: {bubble_region}")
|
||||||
|
# Check if region is valid before attempting screenshot
|
||||||
|
if bubble_region and len(bubble_region) == 4 and bubble_region[2] > 0 and bubble_region[3] > 0:
|
||||||
|
# Convert numpy types to standard Python int for pyautogui
|
||||||
|
region_int = (int(bubble_region[0]), int(bubble_region[1]), int(bubble_region[2]), int(bubble_region[3]))
|
||||||
|
pyautogui.screenshot(region=region_int, imageFilename=screenshot_path)
|
||||||
|
print(f"Successfully saved debug screenshot: {screenshot_path}")
|
||||||
|
screenshot_counter += 1
|
||||||
|
else:
|
||||||
|
print(f"Error: Invalid screenshot region {bubble_region}. Skipping screenshot.")
|
||||||
|
# --- End Enhanced Logging ---
|
||||||
|
except Exception as ss_err:
|
||||||
|
# --- Enhanced Error Logging ---
|
||||||
|
print(f"Error taking debug screenshot for region {bubble_region} to path {screenshot_path}: {repr(ss_err)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc() # Print full traceback for detailed debugging
|
||||||
|
# --- End Enhanced Error Logging ---
|
||||||
|
# --- End Debug Screenshot Logic ---
|
||||||
|
|
||||||
# 4. Interact: Get Bubble Text
|
# 4. Interact: Get Bubble Text
|
||||||
bubble_text = interactor.copy_text_at(keyword_coords)
|
bubble_text = interactor.copy_text_at(keyword_coords)
|
||||||
if not bubble_text:
|
if not bubble_text:
|
||||||
print("Error: Could not get dialogue content.")
|
print("Error: Could not get dialogue content.")
|
||||||
last_processed_bubble_bbox = target_bubble # Mark as processed even if failed
|
last_processed_bubble_info = target_bubble_info # Mark as processed even if failed
|
||||||
perform_state_cleanup(detector, interactor) # Attempt cleanup after failed copy
|
perform_state_cleanup(detector, interactor) # Attempt cleanup after failed copy
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check recent text history (needs context awareness)
|
# Check recent text history (needs context awareness)
|
||||||
if bubble_text in recent_texts:
|
if bubble_text in recent_texts:
|
||||||
print(f"Content '{bubble_text[:30]}...' in recent history, skipping.")
|
print(f"Content '{bubble_text[:30]}...' in recent history, skipping.")
|
||||||
last_processed_bubble_bbox = target_bubble
|
last_processed_bubble_info = target_bubble_info
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(">>> New trigger event <<<")
|
print(">>> New trigger event <<<")
|
||||||
last_processed_bubble_bbox = target_bubble
|
last_processed_bubble_info = target_bubble_info
|
||||||
recent_texts.append(bubble_text)
|
recent_texts.append(bubble_text)
|
||||||
|
|
||||||
# 5. Interact: Get Sender Name
|
# 5. Interact: Get Sender Name
|
||||||
avatar_coords = detector.calculate_avatar_coords(target_bubble)
|
# *** Use the precise TL coordinates for avatar calculation ***
|
||||||
|
avatar_coords = detector.calculate_avatar_coords(target_bubble_info['tl_coords'])
|
||||||
sender_name = interactor.retrieve_sender_name_interaction(avatar_coords)
|
sender_name = interactor.retrieve_sender_name_interaction(avatar_coords)
|
||||||
|
|
||||||
# 6. Perform Cleanup (Crucial after potentially leaving chat screen)
|
# 6. Perform Cleanup (Crucial after potentially leaving chat screen)
|
||||||
@ -569,15 +968,22 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu
|
|||||||
print("\n>>> Putting trigger info in Queue <<<")
|
print("\n>>> Putting trigger info in Queue <<<")
|
||||||
print(f" Sender: {sender_name}")
|
print(f" Sender: {sender_name}")
|
||||||
print(f" Content: {bubble_text[:100]}...")
|
print(f" Content: {bubble_text[:100]}...")
|
||||||
|
print(f" Bubble Region: {bubble_region}") # Include region derived from bbox
|
||||||
try:
|
try:
|
||||||
data_to_send = {'sender': sender_name, 'text': bubble_text}
|
# Include bubble_region in the data sent
|
||||||
|
data_to_send = {
|
||||||
|
'sender': sender_name,
|
||||||
|
'text': bubble_text,
|
||||||
|
'bubble_region': bubble_region # Use bbox-derived region for general use
|
||||||
|
# 'tl_coords': target_bubble_info['tl_coords'] # Optionally send if needed elsewhere
|
||||||
|
}
|
||||||
trigger_queue.put(data_to_send) # Put in the queue for main loop
|
trigger_queue.put(data_to_send) # Put in the queue for main loop
|
||||||
print("Trigger info placed in Queue.")
|
print("Trigger info (with region) placed in Queue.")
|
||||||
except Exception as q_err:
|
except Exception as q_err:
|
||||||
print(f"Error putting data in Queue: {q_err}")
|
print(f"Error putting data in Queue: {q_err}")
|
||||||
|
|
||||||
print("--- Single trigger processing complete ---")
|
print("--- Single trigger processing complete ---")
|
||||||
time.sleep(1) # Pause after successful trigger
|
time.sleep(0.1) # Pause after successful trigger
|
||||||
|
|
||||||
time.sleep(1.5) # Polling interval
|
time.sleep(1.5) # Polling interval
|
||||||
|
|
||||||
@ -591,8 +997,8 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu
|
|||||||
# Attempt cleanup in case of unexpected error during interaction
|
# Attempt cleanup in case of unexpected error during interaction
|
||||||
print("Attempting cleanup after unexpected error...")
|
print("Attempting cleanup after unexpected error...")
|
||||||
perform_state_cleanup(detector, interactor)
|
perform_state_cleanup(detector, interactor)
|
||||||
print("Waiting 5 seconds before retry...")
|
print("Waiting 3 seconds before retry...")
|
||||||
time.sleep(5)
|
time.sleep(3)
|
||||||
|
|
||||||
# Note: The old monitor_chat_for_trigger function is replaced by the example_coordinator_loop.
|
# 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 actual UI monitoring thread started in main.py should call a function like this example loop.
|
||||||
|
|||||||