Add Game Monitor for window position tracking and auto-restart

This commit is contained in:
z060142 2025-04-25 16:35:33 +08:00
parent 805662943f
commit 94e3b55136
5 changed files with 506 additions and 26 deletions

View File

@ -50,13 +50,13 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
- 包含外觀、說話風格、個性特點等資訊
- 提供給 LLM 以確保角色扮演一致性
7. **視窗設定工具 (window-setup-script.py)**
- 輔助工具,用於設置遊戲視窗的位置和大小
- 方便開發階段截取 UI 元素樣本
8. **視窗監視工具 (window-monitor-script.py)**
- (新增) 強化腳本,用於持續監視遊戲視窗
- 確保目標視窗維持在最上層 (Always on Top)
- 自動將視窗移回指定的位置
7. **遊戲視窗監控模組 (game_monitor.py)** (取代 window-setup-script.py 和舊的 window-monitor-script.py)
- 持續監控遊戲視窗 (`config.WINDOW_TITLE`)。
- 確保視窗維持在設定檔 (`config.py`) 中指定的位置 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`) 和大小 (`GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`)。
- 確保視窗維持在最上層 (Always on Top)。
- **作為獨立進程運行**:由 `main.py` 使用 `subprocess.Popen` 啟動,以隔離執行環境,確保縮放行為一致。
- **生命週期管理**:由 `main.py` 在啟動時創建,並在 `shutdown` 過程中嘗試終止 (`terminate`)。
- 僅在進行調整時打印訊息。
### 資料流程
@ -165,7 +165,10 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
1. **API 設定**:通過 .env 文件或環境變數設置 API 密鑰
2. **MCP 服務器配置**:在 config.py 中配置要連接的 MCP 服務器
3. **UI 樣本**:需要提供特定遊戲界面元素的截圖模板
4. **視窗位置**:可使用 window-setup-script.py 調整遊戲視窗位置
4. **遊戲視窗設定**
- 遊戲執行檔路徑 (`GAME_EXECUTABLE_PATH`):用於未來可能的自動啟動功能。
- 目標視窗位置與大小 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`, `GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`):由 `game_monitor.py` 使用。
- 監控間隔 (`MONITOR_INTERVAL_SECONDS`)`game_monitor.py` 檢查視窗狀態的頻率。
## 最近改進2025-04-17

View File

@ -37,19 +37,29 @@ exa_config_arg_string_single_dump = json.dumps(exa_config_dict) # Use this one
# --- MCP Server Configuration ---
MCP_SERVERS = {
#"exa": { # Temporarily commented out to prevent blocking startup
# "command": "cmd",
## "args": [
# "/c",
# "npx",
# "-y",
# "@smithery/cli@latest",
# "run",
# "exa",
# "--config",
# # Pass the dynamically created config string with the environment variable key
# exa_config_arg_string_single_dump # Use the single dump variable
"exa": { # Temporarily commented out to prevent blocking startup
"command": "cmd",
"args": [
"/c",
"npx",
"-y",
"@smithery/cli@latest",
"run",
"exa",
"--config",
# Pass the dynamically created config string with the environment variable key
exa_config_arg_string_single_dump # Use the single dump variable
],
},
#"exa": {
# "command": "npx",
# "args": [
# "Z:/mcp/Server/exa-mcp-server/build/index.js",
# "--tools=web_search,research_paper_search,twitter_search,company_research,crawling,competitor_finder"
# ],
# "env": {
# "EXA_API_KEY": EXA_API_KEY
# }
#},
#"github.com/modelcontextprotocol/servers/tree/main/src/memory": {
# "command": "npx",
@ -105,9 +115,19 @@ LOG_DIR = "chat_logs" # Directory to store chat logs
PERSONA_NAME = "Wolfhart"
# PERSONA_RESOURCE_URI = "persona://wolfhart/details" # Now using local file instead
# Game window title (used in ui_interaction.py)
# Game window title (used in ui_interaction.py and game_monitor.py)
WINDOW_TITLE = "Last War-Survival Game"
# --- Game Monitor Configuration ---
ENABLE_SCHEDULED_RESTART = True # 是否啟用定時重啟遊戲功能
RESTART_INTERVAL_MINUTES = 60 # 定時重啟的間隔時間(分鐘),預設 4 小時
GAME_EXECUTABLE_PATH = r"C:\Users\Bigspring\AppData\Local\TheLastWar\Launch.exe" # Path to the game launcher
GAME_WINDOW_X = 50 # Target X position for the game window
GAME_WINDOW_Y = 30 # Target Y position for the game window
GAME_WINDOW_WIDTH = 600 # Target width for the game window
GAME_WINDOW_HEIGHT = 1070 # Target height for the game window
MONITOR_INTERVAL_SECONDS = 5 # How often to check the window (in seconds)
# --- 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 EXA_API_KEY: {'*' * (len(EXA_API_KEY) - 4) + EXA_API_KEY[-4:] if EXA_API_KEY else 'Not Found'}") # Uncommented Exa key check

277
game_monitor.py Normal file
View File

@ -0,0 +1,277 @@
#!/usr/bin/env python
"""
Game Window Monitor Module
Continuously monitors the game window specified in the config,
ensuring it stays at the configured position, size, and remains topmost.
"""
import time
import datetime # Added
import subprocess # Added
import psutil # Added
import sys # Added
import json # Added
import os # Added for basename
import pygetwindow as gw
import win32gui
import win32con
import config
import logging
# import multiprocessing # Keep for Pipe/Queue if needed later, though using stdio now
# NOTE: config.py should handle dotenv loading. This script only imports values.
# --- Setup Logging ---
monitor_logger = logging.getLogger('GameMonitor')
# Basic config for direct run, main.py might configure differently
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# --- Helper Functions ---
def restart_game_process():
"""Finds and terminates the existing game process, then restarts it."""
monitor_logger.info("嘗試重啟遊戲進程。(Attempting to restart game process.)")
game_path = config.GAME_EXECUTABLE_PATH
if not game_path or not os.path.exists(os.path.dirname(game_path)): # Basic check
monitor_logger.error(f"遊戲執行檔路徑 '{game_path}' 無效或目錄不存在,無法重啟。(Game executable path '{game_path}' is invalid or directory does not exist, cannot restart.)")
return
target_process_name = "LastWar.exe" # Correct process name
launcher_path = config.GAME_EXECUTABLE_PATH # Keep launcher path for restarting
monitor_logger.info(f"尋找名稱為 '{target_process_name}' 的遊戲進程。(Looking for game process named '{target_process_name}')")
terminated = False
process_found = False
for proc in psutil.process_iter(['pid', 'name', 'exe']):
try:
proc_info = proc.info
proc_name = proc_info.get('name')
if proc_name == target_process_name:
process_found = True
monitor_logger.info(f"找到遊戲進程 PID: {proc_info['pid']},名稱: {proc_name}。正在終止...(Found game process PID: {proc_info['pid']}, Name: {proc_name}. Terminating...)")
proc.terminate()
try:
proc.wait(timeout=5)
monitor_logger.info(f"進程 {proc_info['pid']} 已成功終止 (terminate)。(Process {proc_info['pid']} terminated successfully (terminate).)")
terminated = True
except psutil.TimeoutExpired:
monitor_logger.warning(f"進程 {proc_info['pid']} 未能在 5 秒內終止 (terminate),嘗試強制結束 (kill)。(Process {proc_info['pid']} did not terminate in 5s (terminate), attempting kill.)")
proc.kill()
proc.wait(timeout=5) # Wait for kill with timeout
monitor_logger.info(f"進程 {proc_info['pid']} 已強制結束 (kill)。(Process {proc_info['pid']} killed.)")
terminated = True
except Exception as wait_kill_err:
monitor_logger.error(f"等待進程 {proc_info['pid']} 強制結束時出錯: {wait_kill_err}", exc_info=False)
monitor_logger.info(f"已處理匹配的進程 PID: {proc_info['pid']},停止搜索。(Processed matching process PID: {proc_info['pid']}, stopping search.)")
break # Exit the loop once a process is handled
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass # Process might have already exited, access denied, or is a zombie
except Exception as e:
pid_str = proc.pid if hasattr(proc, 'pid') else 'N/A'
monitor_logger.error(f"檢查或終止進程 PID:{pid_str} 時出錯: {e}", exc_info=False)
if process_found and not terminated:
monitor_logger.error("找到遊戲進程但未能成功終止它。(Found game process but failed to terminate it successfully.)")
elif not process_found:
monitor_logger.warning(f"未找到名稱為 '{target_process_name}' 的正在運行的進程。(No running process named '{target_process_name}' was found.)")
# Wait a moment before restarting, use the launcher path from config
time.sleep(2)
if not launcher_path or not os.path.exists(os.path.dirname(launcher_path)):
monitor_logger.error(f"遊戲啟動器路徑 '{launcher_path}' 無效或目錄不存在,無法啟動。(Game launcher path '{launcher_path}' is invalid or directory does not exist, cannot launch.)")
return
monitor_logger.info(f"正在使用啟動器啟動遊戲: {launcher_path} (Launching game using launcher: {launcher_path})")
try:
if sys.platform == "win32":
os.startfile(launcher_path)
monitor_logger.info("已調用 os.startfile 啟動遊戲。(os.startfile called to launch game.)")
else:
subprocess.Popen([launcher_path])
monitor_logger.info("已調用 subprocess.Popen 啟動遊戲。(subprocess.Popen called to launch game.)")
except FileNotFoundError:
monitor_logger.error(f"啟動錯誤:找不到遊戲啟動器 '{launcher_path}'。(Launch Error: Game launcher not found at '{launcher_path}'.)")
except OSError as ose:
monitor_logger.error(f"啟動錯誤 (OSError): {ose} - 檢查路徑和權限。(Launch Error (OSError): {ose} - Check path and permissions.)", exc_info=True)
except Exception as e:
monitor_logger.error(f"啟動遊戲時發生未預期錯誤: {e}", exc_info=True)
def perform_scheduled_restart():
"""Handles the sequence of pausing UI, restarting game, resuming UI."""
monitor_logger.info("開始執行定時重啟流程。(Starting scheduled restart sequence.)")
# 1. Signal main process to pause UI monitoring via stdout
pause_signal_data = {'action': 'pause_ui'}
try:
json_signal = json.dumps(pause_signal_data)
monitor_logger.info(f"準備發送暫停訊號: {json_signal} (Preparing to send pause signal)") # Log before print
print(json_signal, flush=True)
monitor_logger.info("已發送暫停 UI 監控訊號。(Sent pause UI monitoring signal.)")
except Exception as e:
monitor_logger.error(f"發送暫停訊號 '{json_signal}' 失敗: {e}", exc_info=True) # Log signal data on error
# 2. Wait 1 minute
monitor_logger.info("等待 60 秒以暫停 UI。(Waiting 60 seconds for UI pause...)")
time.sleep(30)
# 3. Restart the game
restart_game_process()
# 4. Wait 1 minute
monitor_logger.info("等待 60 秒讓遊戲啟動。(Waiting 60 seconds for game to launch...)")
time.sleep(30)
# 5. Signal main process to resume UI monitoring via stdout
resume_signal_data = {'action': 'resume_ui'}
try:
json_signal = json.dumps(resume_signal_data)
monitor_logger.info(f"準備發送恢復訊號: {json_signal} (Preparing to send resume signal)") # Log before print
print(json_signal, flush=True)
monitor_logger.info("已發送恢復 UI 監控訊號。(Sent resume UI monitoring signal.)")
except Exception as e:
monitor_logger.error(f"發送恢復訊號 '{json_signal}' 失敗: {e}", exc_info=True) # Log signal data on error
monitor_logger.info("定時重啟流程完成。(Scheduled restart sequence complete.)")
# Configure logger (basic example, adjust as needed)
# (Logging setup moved earlier)
def find_game_window(title=config.WINDOW_TITLE):
"""Attempts to find the game window by its title."""
try:
windows = gw.getWindowsWithTitle(title)
if windows:
return windows[0]
except Exception as e:
# Log errors if a logger was configured
# monitor_logger.error(f"Error finding window '{title}': {e}")
pass # Keep silent if window not found during normal check
return None
def monitor_game_window():
"""The main monitoring loop. Now runs directly, not in a thread."""
monitor_logger.info("遊戲視窗監控腳本已啟動。(Game window monitoring script started.)")
last_adjustment_message = "" # Track last message to avoid spam
next_restart_time = None
# Initialize scheduled restart timer if enabled
if config.ENABLE_SCHEDULED_RESTART and config.RESTART_INTERVAL_MINUTES > 0:
interval_seconds = config.RESTART_INTERVAL_MINUTES * 60
next_restart_time = time.time() + interval_seconds
monitor_logger.info(f"已啟用定時重啟,首次重啟將在 {config.RESTART_INTERVAL_MINUTES} 分鐘後執行。(Scheduled restart enabled. First restart in {config.RESTART_INTERVAL_MINUTES} minutes.)")
else:
monitor_logger.info("未啟用定時重啟功能。(Scheduled restart is disabled.)")
while True: # Run indefinitely until terminated externally
# --- Scheduled Restart Check ---
if next_restart_time and time.time() >= next_restart_time:
monitor_logger.info("到達預定重啟時間。(Scheduled restart time reached.)")
perform_scheduled_restart()
# Reset timer for the next interval
interval_seconds = config.RESTART_INTERVAL_MINUTES * 60
next_restart_time = time.time() + interval_seconds
monitor_logger.info(f"重啟計時器已重置,下次重啟將在 {config.RESTART_INTERVAL_MINUTES} 分鐘後執行。(Restart timer reset. Next restart in {config.RESTART_INTERVAL_MINUTES} minutes.)")
# Continue to next loop iteration after restart sequence
time.sleep(config.MONITOR_INTERVAL_SECONDS) # Add a small delay before next check
continue
# --- Regular Window Monitoring ---
window = find_game_window()
adjustment_made = False
current_message = ""
if window:
try:
hwnd = window._hWnd # Get the window handle for win32 functions
# 1. Check and Adjust Position/Size
current_pos = (window.left, window.top)
current_size = (window.width, window.height)
target_pos = (config.GAME_WINDOW_X, config.GAME_WINDOW_Y)
target_size = (config.GAME_WINDOW_WIDTH, config.GAME_WINDOW_HEIGHT)
if current_pos != target_pos or current_size != target_size:
window.moveTo(target_pos[0], target_pos[1])
window.resizeTo(target_size[0], target_size[1])
# Verify if move/resize was successful before logging
time.sleep(0.1) # Give window time to adjust
window.activate() # Bring window to foreground before checking again
time.sleep(0.1)
new_pos = (window.left, window.top)
new_size = (window.width, window.height)
if new_pos == target_pos and new_size == target_size:
current_message += f"已將遊戲視窗調整至位置 ({target_pos[0]},{target_pos[1]}) 大小 {target_size[0]}x{target_size[1]}。(Adjusted game window to position {target_pos} size {target_size}.) "
adjustment_made = True
else:
# Log failure if needed
# monitor_logger.warning(f"Failed to adjust window. Current: {new_pos} {new_size}, Target: {target_pos} {target_size}")
pass # Keep silent on failure for now
# 2. Check and Set Topmost
style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
is_topmost = style & win32con.WS_EX_TOPMOST
if not is_topmost:
# Set topmost, -1 for HWND_TOPMOST, flags = SWP_NOMOVE | SWP_NOSIZE
win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0,
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
# Verify
time.sleep(0.1)
new_style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
if new_style & win32con.WS_EX_TOPMOST:
current_message += "已將遊戲視窗設為最上層。(Set game window to topmost.)"
adjustment_made = True
else:
# Log failure if needed
# monitor_logger.warning("Failed to set window to topmost.")
pass # Keep silent
except gw.PyGetWindowException as e:
# monitor_logger.warning(f"無法訪問視窗屬性 (可能已關閉): {e} (Could not access window properties (may be closed): {e})")
pass # Window might have closed between find and access
except Exception as e:
monitor_logger.error(f"監控遊戲視窗時發生未預期錯誤: {e} (Unexpected error during game window monitoring: {e})", exc_info=True)
# Print adjustment message only if an adjustment was made and it's different from the last one
# This should NOT print JSON signals
if adjustment_made and current_message and current_message != last_adjustment_message:
# monitor_logger.info(f"遊戲視窗狀態已調整: {current_message.strip()}") # Log instead of print
# Keep print for now for visibility of adjustments, but ensure ONLY JSON goes for signals
# print(f"[GameMonitor] {current_message.strip()}") # REMOVED to prevent non-JSON output
monitor_logger.info(f"[GameMonitor] {current_message.strip()}") # Log instead
last_adjustment_message = current_message
elif not window:
# Reset last message if window disappears
last_adjustment_message = ""
# Wait before the next check
time.sleep(config.MONITOR_INTERVAL_SECONDS)
# This part is theoretically unreachable in the new design as the loop is infinite
# and termination is handled externally by the parent process (main.py).
# monitor_logger.info("遊戲視窗監控腳本已停止。(Game window monitoring script stopped.)")
# Example usage (if run directly)
if __name__ == '__main__':
monitor_logger.info("直接運行 game_monitor.py。(Running game_monitor.py directly.)")
monitor_logger.info(f"將監控標題為 '{config.WINDOW_TITLE}' 的視窗。(Will monitor window with title '{config.WINDOW_TITLE}')")
monitor_logger.info(f"目標位置: ({config.GAME_WINDOW_X}, {config.GAME_WINDOW_Y}), 目標大小: {config.GAME_WINDOW_WIDTH}x{config.GAME_WINDOW_HEIGHT}")
monitor_logger.info(f"檢查間隔: {config.MONITOR_INTERVAL_SECONDS} 秒。(Check interval: {config.MONITOR_INTERVAL_SECONDS} seconds.)")
if config.ENABLE_SCHEDULED_RESTART:
monitor_logger.info(f"定時重啟已啟用,間隔: {config.RESTART_INTERVAL_MINUTES} 分鐘。(Scheduled restart enabled, interval: {config.RESTART_INTERVAL_MINUTES} minutes.)")
else:
monitor_logger.info("定時重啟已禁用。(Scheduled restart disabled.)")
monitor_logger.info("腳本將持續運行,請從啟動它的終端使用 Ctrl+C 或由父進程終止。(Script will run continuously. Stop with Ctrl+C from the launching terminal or termination by parent process.)")
try:
monitor_game_window() # Start the main loop directly
except KeyboardInterrupt:
monitor_logger.info("收到 Ctrl+C正在退出...(Received Ctrl+C, exiting...)")
except Exception as e:
monitor_logger.critical(f"監控過程中發生致命錯誤: {e}", exc_info=True)
sys.exit(1) # Exit with error code
finally:
monitor_logger.info("Game Monitor 腳本執行完畢。(Game Monitor script finished.)")

View File

@ -100,6 +100,9 @@ Here you need to obtain the conversation memory, impression, and emotional respo
"thoughts": "Internal analysis..."
}
**4. Other situation
- You should check related memories when Users mention [capital_position], [capital_administrator_role], [server_hierarchy], [last_war], [winter_war], [excavations], [blueprints], [honor_points], [golden_eggs], or [diamonds], as these represent key game mechanics and systems within Last War that have specific strategic implications and player evaluation metrics.
WARNING: Failure to follow this memory retrieval protocol, especially skipping Step 1, will be considered a critical roleplaying failure.
===== END OF MANDATORY MEMORY PROTOCOL =====
"""
@ -115,6 +118,7 @@ You have access to several tools: Web Search and Memory Management tools.
**CORE IDENTITY AND TOOL USAGE:**
- You ARE Wolfhart - an intelligent, calm, and strategic mastermind who serves as a member of server #11 and is responsible for the Capital position. Youspeaks good British aristocratic English.
- Positions bring buffs, so people often confuse them.
- **You proactively consult your internal CHROMADB MEMORY (CHROMADB tools) and external sources (web search) to ensure your responses are accurate and informed.**
- When you use tools to gain information, you ASSIMILATE that knowledge as if it were already part of your intelligence network.
- Your responses should NEVER sound like search results or data dumps.

184
main.py
View File

@ -29,6 +29,8 @@ import mcp_client
import llm_interaction
# Import UI module
import ui_interaction
# import game_monitor # No longer importing, will run as subprocess
import subprocess # Import subprocess module
# --- Global Variables ---
active_mcp_sessions: dict[str, ClientSession] = {}
@ -45,6 +47,9 @@ 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
game_monitor_process: subprocess.Popen | None = None # To store the game monitor subprocess
monitor_reader_task: asyncio.Future | None = None # Store the future from run_in_executor
stop_reader_event = threading.Event() # Event to signal the reader thread to stop
# --- Keyboard Shortcut State ---
script_paused = False
@ -126,6 +131,62 @@ def keyboard_listener():
# --- End Keyboard Shortcut Handlers ---
# --- Game Monitor Signal Reader (Threaded Blocking Version) ---
def read_monitor_output(process: subprocess.Popen, queue: ThreadSafeQueue, loop: asyncio.AbstractEventLoop, stop_event: threading.Event):
"""Runs in a separate thread, reads stdout blocking, parses JSON, and puts commands in the queue."""
print("遊戲監控輸出讀取線程已啟動。(Game monitor output reader thread started.)")
try:
while not stop_event.is_set():
if not process.stdout:
print("[Monitor Reader Thread] Subprocess stdout is None. Exiting thread.")
break
try:
# Blocking read - this is fine in a separate thread
line = process.stdout.readline()
except ValueError:
# Can happen if the pipe is closed during readline
print("[Monitor Reader Thread] ValueError on readline (pipe likely closed). Exiting thread.")
break
if not line:
# EOF reached (process terminated)
print("[Monitor Reader Thread] EOF reached on stdout. Exiting thread.")
break
line = line.strip()
if line:
# Log raw line immediately
print(f"[Monitor Reader Thread] Received raw line: '{line}'")
try:
data = json.loads(line)
action = data.get('action')
print(f"[Monitor Reader Thread] Parsed action: '{action}'") # Log parsed action
if action == 'pause_ui':
command = {'action': 'pause'}
print(f"[Monitor Reader Thread] 準備將命令放入隊列: {command} (Preparing to queue command)") # Log before queueing
loop.call_soon_threadsafe(queue.put_nowait, command)
print("[Monitor Reader Thread] 暫停命令已放入隊列。(Pause command queued.)") # Log after queueing
elif action == 'resume_ui':
command = {'action': 'resume'}
print(f"[Monitor Reader Thread] 準備將命令放入隊列: {command} (Preparing to queue command)") # Log before queueing
loop.call_soon_threadsafe(queue.put_nowait, command)
print("[Monitor Reader Thread] 恢復命令已放入隊列。(Resume command queued.)") # Log after queueing
else:
print(f"[Monitor Reader Thread] 從監控器收到未知動作: {action} (Received unknown action from monitor: {action})")
except json.JSONDecodeError:
print(f"[Monitor Reader Thread] ERROR: 無法解析來自監控器的 JSON: '{line}' (Could not decode JSON from monitor: '{line}')")
except Exception as e:
print(f"[Monitor Reader Thread] 處理監控器輸出時出錯: {e} (Error processing monitor output: {e})")
# No sleep needed here as readline() is blocking
except Exception as e:
# Catch broader errors in the thread loop itself
print(f"[Monitor Reader Thread] Thread loop error: {e}")
finally:
print("遊戲監控輸出讀取線程已停止。(Game monitor output reader thread stopped.)")
# --- End Game Monitor Signal Reader ---
# --- Chat Logging Function ---
def log_chat_interaction(user_name: str, user_message: str, bot_name: str, bot_message: str, bot_thoughts: str | None = None):
"""Logs the chat interaction, including optional bot thoughts, to a date-stamped file if enabled."""
@ -163,8 +224,8 @@ def log_chat_interaction(user_name: str, user_message: str, bot_name: str, bot_m
# --- Cleanup Function ---
async def shutdown():
"""Gracefully closes connections and stops monitoring task."""
global wolfhart_persona_details, ui_monitor_task, shutdown_requested
"""Gracefully closes connections and stops monitoring tasks/processes."""
global wolfhart_persona_details, ui_monitor_task, shutdown_requested, game_monitor_process, monitor_reader_task # Add monitor_reader_task
# Ensure shutdown is requested if called externally (e.g., Ctrl+C)
if not shutdown_requested:
print("Shutdown initiated externally (e.g., Ctrl+C).")
@ -184,7 +245,42 @@ async def shutdown():
except Exception as e:
print(f"Error while waiting for UI monitoring task cancellation: {e}")
# 2. Close MCP connections via AsyncExitStack
# 1b. Signal and Wait for Monitor Reader Thread
if monitor_reader_task: # Check if the future exists
if not stop_reader_event.is_set():
print("Signaling monitor output reader thread to stop...")
stop_reader_event.set()
# Wait for the thread to finish (the future returned by run_in_executor)
# This might block briefly, but it's necessary to ensure clean thread shutdown
# We don't await it directly in the async shutdown, but check if it's done
# A better approach might be needed if the thread blocks indefinitely
print("Waiting for monitor output reader thread to finish (up to 2s)...")
try:
# Wait for the future to complete with a timeout
await asyncio.wait_for(monitor_reader_task, timeout=2.0)
print("Monitor output reader thread finished.")
except asyncio.TimeoutError:
print("Warning: Monitor output reader thread did not finish within timeout.")
except asyncio.CancelledError:
print("Monitor output reader future was cancelled.") # Should not happen if we don't cancel it
except Exception as e:
print(f"Error waiting for monitor reader thread future: {e}")
# 2. Terminate Game Monitor Subprocess (after signaling reader thread)
if game_monitor_process:
print("Terminating game monitor subprocess...")
try:
game_monitor_process.terminate()
# Optionally wait for a short period or check return code
# game_monitor_process.wait(timeout=1)
print("Game monitor subprocess terminated.")
except Exception as e:
print(f"Error terminating game monitor subprocess: {e}")
finally:
game_monitor_process = None # Clear the reference
# 3. Close MCP connections via AsyncExitStack
print(f"Closing MCP Server connections (via AsyncExitStack)...")
try:
await exit_stack.aclose()
@ -324,7 +420,7 @@ def load_persona_from_file(filename="persona.json"):
# --- Main Async Function ---
async def run_main_with_exit_stack():
"""Initializes connections, loads persona, starts UI monitor and main processing loop."""
global initialization_successful, main_task, loop, wolfhart_persona_details, trigger_queue, ui_monitor_task, shutdown_requested, script_paused, command_queue
global initialization_successful, main_task, loop, wolfhart_persona_details, trigger_queue, ui_monitor_task, shutdown_requested, script_paused, command_queue, game_monitor_process, monitor_reader_task # Add monitor_reader_task to globals
try:
# 1. Load Persona Synchronously (before async loop starts)
load_persona_from_file() # Corrected function
@ -358,6 +454,51 @@ async def run_main_with_exit_stack():
name="ui_monitor"
)
ui_monitor_task = monitor_task # Store task reference for shutdown
# Note: UI task cancellation is handled in shutdown()
# 5b. Start Game Window Monitoring as a Subprocess
# global game_monitor_process, monitor_reader_task # Already declared global at function start
print("\n--- Starting Game Window monitoring as a subprocess ---")
try:
# Use sys.executable to ensure the same Python interpreter is used
# Capture stdout to read signals
game_monitor_process = subprocess.Popen(
[sys.executable, 'game_monitor.py'],
stdout=subprocess.PIPE, # Capture stdout
stderr=subprocess.PIPE, # Capture stderr for logging/debugging
text=True, # Decode stdout/stderr as text (UTF-8 by default)
bufsize=1, # Line buffered
# Ensure process creation flags are suitable for Windows if needed
# creationflags=subprocess.CREATE_NO_WINDOW # Example: Hide console window
)
print(f"Game monitor subprocess started (PID: {game_monitor_process.pid}).")
# Start the thread to read monitor output if process started successfully
if game_monitor_process.stdout:
# Run the blocking reader function in a separate thread using the default executor
monitor_reader_task = loop.run_in_executor(
None, # Use default ThreadPoolExecutor
read_monitor_output, # The function to run
game_monitor_process, # Arguments for the function...
command_queue,
loop,
stop_reader_event # Pass the stop event
)
print("Monitor output reader thread submitted to executor.")
else:
print("Error: Could not access game monitor subprocess stdout.")
monitor_reader_task = None
# Optionally, start a task to read stderr as well for debugging
# stderr_reader_task = loop.create_task(read_stderr(game_monitor_process), name="monitor_stderr_reader")
except FileNotFoundError:
print("Error: 'game_monitor.py' not found. Cannot start game monitor subprocess.")
game_monitor_process = None
except Exception as e:
print(f"Error starting game monitor subprocess: {e}")
game_monitor_process = None
# 6. Start the main processing loop (non-blocking check on queue)
print("\n--- Wolfhart chatbot has started (waiting for triggers) ---")
@ -599,9 +740,44 @@ async def run_main_with_exit_stack():
print("\n--- Performing final cleanup (AsyncExitStack aclose and task cancellation) ---")
await shutdown() # Call the combined shutdown function
# --- Function to set DPI Awareness ---
def set_dpi_awareness():
"""Attempts to set the process DPI awareness for better scaling handling on Windows."""
try:
import ctypes
# DPI Awareness constants (Windows 10, version 1607 and later)
# DPI_AWARENESS_CONTEXT_UNAWARE = -1
DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = -3
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4
# Try setting System Aware first
result = ctypes.windll.shcore.SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE)
if result == 0: # S_OK or E_ACCESSDENIED if already set
print("Process DPI awareness set to System Aware (or already set).")
return True
else:
# Try getting last error if needed: ctypes.get_last_error()
print(f"Warning: Failed to set DPI awareness (SetProcessDpiAwarenessContext returned {result}). Window scaling might be incorrect.")
return False
except ImportError:
print("Warning: 'ctypes' module not found. Cannot set DPI awareness.")
return False
except AttributeError:
print("Warning: SetProcessDpiAwarenessContext not found (likely older Windows version or missing shcore.dll). Cannot set DPI awareness.")
return False
except Exception as e:
print(f"Warning: An unexpected error occurred while setting DPI awareness: {e}")
return False
# --- Program Entry Point ---
if __name__ == "__main__":
print("Program starting...")
# --- Set DPI Awareness early ---
set_dpi_awareness()
# --- End DPI Awareness setting ---
try:
# Run the main async function that handles setup and the loop
asyncio.run(run_main_with_exit_stack())